@waterwx/dsh-novel-forge 1.7.0 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/client.js +3197 -1887
- package/lib/client.js.map +1 -1
- package/lib/index.js +1026 -84
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +2 -0
- package/lib/types/client/locales.d.ts +0 -1
- package/lib/types/client/panel/FlowGuide.d.ts +21 -0
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +18 -0
- package/lib/types/client/panel/MangaWorkspace.d.ts +2 -2
- package/lib/types/client/panel/StoryboardTab.d.ts +5 -1
- package/lib/types/engine.d.ts +15 -8
- package/lib/types/protocol.d.ts +138 -10
- package/lib/types/shot-language.d.ts +46 -0
- package/lib/types/story-beat-language.d.ts +35 -0
- package/package.json +5 -2
- package/skills/chapter-batch/SKILL.md +81 -0
- package/src/client/api.ts +5 -0
- package/src/client/locales.ts +0 -1
- package/src/client/panel/AssetsTab.tsx +22 -22
- package/src/client/panel/AssistantTab.tsx +3 -3
- package/src/client/panel/BookshelfBar.tsx +1 -1
- package/src/client/panel/CreateBookView.tsx +14 -10
- package/src/client/panel/FlowGuide.tsx +83 -0
- package/src/client/panel/ImportModal.tsx +11 -2
- package/src/client/panel/MangaRoleLibrary.tsx +810 -0
- package/src/client/panel/MangaWorkspace.tsx +359 -297
- package/src/client/panel/NovelPanel.tsx +108 -107
- package/src/client/panel/ReaderView.tsx +4 -4
- package/src/client/panel/RunPanel.tsx +9 -9
- package/src/client/panel/SceneLibrary.tsx +68 -67
- package/src/client/panel/ShelfView.tsx +2 -2
- package/src/client/panel/StoryboardTab.tsx +145 -49
- package/src/client/panel/StyleCard.tsx +13 -13
- package/src/client/panel/StyleGallery.tsx +2 -2
- package/src/client/panel/WorldTab.tsx +5 -5
- package/src/client/panel/panel.module.css +861 -510
- package/src/client/panel/views.tsx +24 -24
- package/src/engine.ts +514 -62
- package/src/index.ts +1 -0
- package/src/protocol.ts +147 -10
- package/src/routes.ts +216 -7
- package/src/run.ts +5 -4
- package/src/shot-language.ts +181 -0
- package/src/story-beat-language.ts +130 -0
- package/lib/types/client/panel/RoleVisualPanel.d.ts +0 -15
- package/src/client/panel/RoleVisualPanel.tsx +0 -485
|
@@ -5,8 +5,25 @@
|
|
|
5
5
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
6
6
|
import type { NovelApi } from '../api.ts'
|
|
7
7
|
import type { ChapterPlan, ProjectState, StoryboardPrompt, StoryboardSkeleton, StoryboardTable } from '../../protocol.ts'
|
|
8
|
+
import {
|
|
9
|
+
sizeZh, cameraZh, compoZh, lightZh,
|
|
10
|
+
normalizeShotSize, normalizeCameras, normalizeComposition, normalizeLightings,
|
|
11
|
+
} from '../../shot-language.ts'
|
|
12
|
+
import { functionZh, emotionZh, normalizeStoryFunction, normalizeEmotions } from '../../story-beat-language.ts'
|
|
13
|
+
import type { StoryboardShot } from '../../protocol.ts'
|
|
8
14
|
import css from './panel.module.css'
|
|
9
15
|
|
|
16
|
+
/** 归一化旧分镜数据:把自由文本 shot/camera/light/composition 转成词库枚举。 */
|
|
17
|
+
function normalizeStoryboardShot(s: StoryboardShot): StoryboardShot {
|
|
18
|
+
return {
|
|
19
|
+
...s,
|
|
20
|
+
shot: normalizeShotSize(typeof s.shot === 'string' ? s.shot : undefined),
|
|
21
|
+
camera: normalizeCameras(typeof s.camera === 'string' ? s.camera : undefined),
|
|
22
|
+
composition: typeof s.composition === 'string' ? normalizeComposition(s.composition) : s.composition,
|
|
23
|
+
light: normalizeLightings(typeof s.light === 'string' ? s.light : undefined),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
export function StoryboardTab({
|
|
11
28
|
api,
|
|
12
29
|
project,
|
|
@@ -14,6 +31,8 @@ export function StoryboardTab({
|
|
|
14
31
|
onProjectChanged,
|
|
15
32
|
styleId,
|
|
16
33
|
filterId,
|
|
34
|
+
mode,
|
|
35
|
+
onGoStep,
|
|
17
36
|
onProgress,
|
|
18
37
|
}: {
|
|
19
38
|
api: NovelApi
|
|
@@ -25,10 +44,21 @@ export function StoryboardTab({
|
|
|
25
44
|
styleId?: string
|
|
26
45
|
/** 可选滤镜风格 id。 */
|
|
27
46
|
filterId?: string
|
|
47
|
+
/** 页面模式:auto=按本章实际进度显示;skeleton/table/prompts=固定显示该步骤(前置不足显示提示,不降级)。 */
|
|
48
|
+
mode?: 'auto' | 'skeleton' | 'table' | 'prompts'
|
|
49
|
+
/** 「下一步」按钮回调(1=骨架→2=分镜表→3=提示词),供外层切步骤页。 */
|
|
50
|
+
onGoStep?: (n: 1 | 2 | 3) => void
|
|
28
51
|
/** 上报到「AI进度」控制台(分镜三步生成)。 */
|
|
29
52
|
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
30
53
|
}) {
|
|
31
54
|
const written = useMemo(() => chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error').sort((a, b) => a.no - b.no), [chapters])
|
|
55
|
+
/** 漫剧卡 id → 卡名/参考图(定妆绑定展示)。 */
|
|
56
|
+
const mangaById = useMemo(() => {
|
|
57
|
+
const m = new Map<string, { name: string; imageUrl?: string }>()
|
|
58
|
+
for (const c of project?.mangaRoles ?? []) m.set(c.id, { name: c.name, imageUrl: c.imageUrl })
|
|
59
|
+
return m
|
|
60
|
+
}, [project?.mangaRoles])
|
|
61
|
+
const bindingNames = (ids: string[] | undefined): string => (ids ?? []).map(id => mangaById.get(id)?.name ?? id).join('、')
|
|
32
62
|
const [chapterNo, setChapterNo] = useState<number | null>(written[0]?.no ?? null)
|
|
33
63
|
const [busy, setBusy] = useState(false)
|
|
34
64
|
const [error, setError] = useState('')
|
|
@@ -63,8 +93,23 @@ export function StoryboardTab({
|
|
|
63
93
|
if (chapterNo === null) return
|
|
64
94
|
if (suppressRestoreRef.current === true) return
|
|
65
95
|
const entry = (project?.storyboards ?? []).find(e => e.chapterNo === chapterNo)
|
|
66
|
-
setSkeleton(prev =>
|
|
67
|
-
|
|
96
|
+
setSkeleton(prev => {
|
|
97
|
+
if (prev !== null) return prev
|
|
98
|
+
if (entry?.skeleton === undefined) return null
|
|
99
|
+
return {
|
|
100
|
+
...entry.skeleton,
|
|
101
|
+
beats: (entry.skeleton.beats ?? []).map(b => ({
|
|
102
|
+
...b,
|
|
103
|
+
function: normalizeStoryFunction(typeof b.function === 'string' ? b.function : undefined),
|
|
104
|
+
emotion: normalizeEmotions(typeof b.emotion === 'string' ? b.emotion : b.emotion?.join('→')),
|
|
105
|
+
})),
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
setTable(prev => {
|
|
109
|
+
if (prev !== null) return prev
|
|
110
|
+
if (entry?.table === undefined) return null
|
|
111
|
+
return { ...entry.table, shots: (entry.table.shots ?? []).map(normalizeStoryboardShot) }
|
|
112
|
+
})
|
|
68
113
|
setPrompts(prev => prev ?? entry?.prompts ?? null)
|
|
69
114
|
}, [chapterNo, project?.storyboards])
|
|
70
115
|
|
|
@@ -149,7 +194,9 @@ export function StoryboardTab({
|
|
|
149
194
|
return <div className={css.card}><span className={css.meta}>请先开书或选择一本书,再进入分镜工作台。</span></div>
|
|
150
195
|
}
|
|
151
196
|
const maxStep = prompts !== null ? 3 : table !== null ? 2 : skeleton !== null ? 1 : 0
|
|
152
|
-
|
|
197
|
+
// 固定模式:只显示指定步骤(前置不足由黄条提示,不降级);auto:按本章实际进度显示。
|
|
198
|
+
const modeStep = mode === 'skeleton' ? 1 : mode === 'table' ? 2 : mode === 'prompts' ? 3 : null
|
|
199
|
+
const step = modeStep !== null ? modeStep : Math.min(stepState, Math.max(maxStep, 1))
|
|
153
200
|
|
|
154
201
|
return (
|
|
155
202
|
<div className={css.card}>
|
|
@@ -158,21 +205,13 @@ export function StoryboardTab({
|
|
|
158
205
|
<span className={css.meta}>三步向导 · {styleId !== undefined ? '按当前方案风格生成' : '未选方案风格'}</span>
|
|
159
206
|
</div>
|
|
160
207
|
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
disabled={n > maxStep}
|
|
169
|
-
onClick={() => { setStepState(n) }}
|
|
170
|
-
>
|
|
171
|
-
{n === 1 ? '① 剧情骨架' : n === 2 ? '② 分镜表' : '③ 视频提示词'}
|
|
172
|
-
{n <= maxStep && step !== n ? ' ✓' : ''}
|
|
173
|
-
</button>
|
|
174
|
-
))}
|
|
175
|
-
</div>
|
|
208
|
+
<span className={css.meta} style={{ display: 'block', marginBottom: 'var(--nf-space-6)' }}>
|
|
209
|
+
{step === 1
|
|
210
|
+
? '这步:从章节正文提炼剧情骨架(弧线/节拍/出场角色)· 前置:已写章节 · 下一步:生成分镜表'
|
|
211
|
+
: step === 2
|
|
212
|
+
? '这步:骨架展开为镜头级分镜表(景别/机位/台词/每镜头角色+定妆绑定)· 前置:剧情骨架 · 下一步:视频提示词'
|
|
213
|
+
: '这步:分镜表 → 即梦可粘贴视频提示词(带风格词块与定妆绑定)· 前置:分镜表(建议先完成角色定妆)· 下一步:复制到即梦/豆包'}
|
|
214
|
+
</span>
|
|
176
215
|
|
|
177
216
|
<div className={css.row} style={{ alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
|
178
217
|
<div className={css.field} style={{ flex: 1, minWidth: 200 }}>
|
|
@@ -188,39 +227,81 @@ export function StoryboardTab({
|
|
|
188
227
|
))}
|
|
189
228
|
</select>
|
|
190
229
|
</div>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
230
|
+
{/* 生成/重新生成骨架只属于骨架页(③);分镜表/提示词页由各自内容区的按钮负责 */}
|
|
231
|
+
{mode !== 'table' && mode !== 'prompts' && (
|
|
232
|
+
<>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className={`${css.button} ${css.buttonPrimary}`}
|
|
236
|
+
disabled={busy || chapterNo === null}
|
|
237
|
+
onClick={() => { void generate(false) }}
|
|
238
|
+
>
|
|
239
|
+
{busy ? '编剧分析中…' : '✍️ 生成剧情骨架'}
|
|
240
|
+
</button>
|
|
241
|
+
{skeleton !== null && (
|
|
242
|
+
<button type="button" className={css.button} disabled={busy} onClick={() => { void generate(true) }}>
|
|
243
|
+
🔄 重新生成(并继续生成分镜表)
|
|
244
|
+
</button>
|
|
245
|
+
)}
|
|
246
|
+
</>
|
|
203
247
|
)}
|
|
204
248
|
</div>
|
|
205
249
|
|
|
206
250
|
{error !== '' && <div className={css.importError}>{error}</div>}
|
|
207
251
|
|
|
252
|
+
{/* 固定模式下的前置不足提示(不静默降级) */}
|
|
253
|
+
{mode === 'table' && skeleton === null && (
|
|
254
|
+
<div style={{ border: '1px solid var(--nf-warn, #b8860b)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', marginTop: 'var(--nf-space-6)' }}>
|
|
255
|
+
<span className={css.meta}>⚠ 本章还没有剧情骨架——请先到「③ 剧情骨架」页生成骨架,再回来生成分镜表。</span>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
{mode === 'prompts' && table === null && (
|
|
259
|
+
<div style={{ border: '1px solid var(--nf-warn, #b8860b)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', marginTop: 'var(--nf-space-6)' }}>
|
|
260
|
+
<span className={css.meta}>⚠ 本章还没有分镜表——请先完成「③ 剧情骨架」「④ 分镜表」,再回来生成视频提示词。</span>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
{/* 固定模式:前置已满足但本步产物缺失 → 直接给生成按钮 */}
|
|
264
|
+
{mode === 'table' && skeleton !== null && table === null && (
|
|
265
|
+
<div className={css.row} style={{ marginTop: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
266
|
+
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={tableBusy} onClick={() => { void generateTable() }}>
|
|
267
|
+
{tableBusy ? '生成中…' : '🎬 生成分镜表'}
|
|
268
|
+
</button>
|
|
269
|
+
<span className={css.meta}>骨架已就绪,本章还没有分镜表——点按钮生成。</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
{mode === 'prompts' && table !== null && prompts === null && (
|
|
273
|
+
<div className={css.row} style={{ marginTop: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
274
|
+
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={promptsBusy} onClick={() => { void generatePrompts() }}>
|
|
275
|
+
{promptsBusy ? '生成中…' : '🎬 生成视频提示词'}
|
|
276
|
+
</button>
|
|
277
|
+
<span className={css.meta}>分镜表已就绪,本章还没有视频提示词——点按钮生成。</span>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
208
281
|
{step === 1 && (skeleton === null ? (
|
|
209
|
-
<div className={css.meta} style={{ marginTop: 8 }}>
|
|
282
|
+
<div className={css.meta} style={{ marginTop: 'var(--nf-space-8)' }}>
|
|
210
283
|
生成后这里显示本章剧情骨架:弧线 + 节拍链(事件 / 情绪走向 / 叙事功能 / 因果)。
|
|
211
284
|
骨架是「完整剧情」的根——确认骨架没问题后,进入下一步展开为分镜表。
|
|
212
285
|
</div>
|
|
213
286
|
) : (
|
|
214
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}>
|
|
287
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', marginTop: 'var(--nf-space-8)' }}>
|
|
215
288
|
<div className={css.importPreview}>
|
|
216
289
|
<span>📖 本章弧线:{skeleton.arc}</span>
|
|
217
290
|
</div>
|
|
291
|
+
{skeleton.characters !== undefined && skeleton.characters.length > 0 && (
|
|
292
|
+
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
293
|
+
<span className={css.meta}>👥 出场角色:</span>
|
|
294
|
+
{skeleton.characters.map(c => (
|
|
295
|
+
<span key={c} className={css.badge}>{c}</span>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
218
299
|
{skeleton.beats.map((b, i) => (
|
|
219
|
-
<div key={b.id} style={{ display: 'flex', flexDirection: 'column', gap:
|
|
300
|
+
<div key={b.id} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)' }}>
|
|
220
301
|
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
221
302
|
<b>节拍 {i + 1}</b>
|
|
222
|
-
<span className={`${css.badge} ${b.function === '
|
|
223
|
-
<span className={css.meta}
|
|
303
|
+
<span className={`${css.badge} ${b.function === 'climax' ? css.badgeDone : b.function === 'turn' ? css.badgeWritten : css.badgePending}`}>{functionZh(b.function)}</span>
|
|
304
|
+
<span className={css.meta}>💭 {emotionZh(b.emotion)}</span>
|
|
224
305
|
</div>
|
|
225
306
|
<span>{b.event}</span>
|
|
226
307
|
{b.cause !== undefined && <span className={css.meta}>承接:{b.cause}</span>}
|
|
@@ -231,7 +312,7 @@ export function StoryboardTab({
|
|
|
231
312
|
type="button"
|
|
232
313
|
className={`${css.button} ${css.buttonSmall}`}
|
|
233
314
|
onClick={() => {
|
|
234
|
-
const text = `第${skeleton.chapterNo}章 弧线:${skeleton.arc}\n\n` + skeleton.beats.map((b, i) => `${i + 1}. [${b.function}] ${b.event}(情绪:${b.emotion})${b.cause !== undefined ? `[承接:${b.cause}]` : ''}`).join('\n')
|
|
315
|
+
const text = `第${skeleton.chapterNo}章 弧线:${skeleton.arc}\n出场角色:${skeleton.characters !== undefined && skeleton.characters.length > 0 ? skeleton.characters.join('、') : '(未标注)'}\n\n` + skeleton.beats.map((b, i) => `${i + 1}. [${functionZh(b.function)}] ${b.event}(情绪:${emotionZh(b.emotion)})${b.cause !== undefined ? `[承接:${b.cause}]` : ''}`).join('\n')
|
|
235
316
|
void navigator.clipboard?.writeText(text)
|
|
236
317
|
}}
|
|
237
318
|
>
|
|
@@ -240,7 +321,7 @@ export function StoryboardTab({
|
|
|
240
321
|
<button
|
|
241
322
|
type="button"
|
|
242
323
|
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
243
|
-
onClick={() => {
|
|
324
|
+
onClick={() => { if (table === null) void generateTable(); onGoStep?.(2) }}
|
|
244
325
|
>
|
|
245
326
|
🎬 下一步:生成分镜表
|
|
246
327
|
</button>
|
|
@@ -250,7 +331,7 @@ export function StoryboardTab({
|
|
|
250
331
|
))}
|
|
251
332
|
|
|
252
333
|
{step === 2 && table !== null && (
|
|
253
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 10 }}>
|
|
334
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)', marginTop: 'var(--nf-space-10)' }}>
|
|
254
335
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
255
336
|
<b>🎬 分镜表({table.shots.length} 个镜头)</b>
|
|
256
337
|
<button
|
|
@@ -269,7 +350,7 @@ export function StoryboardTab({
|
|
|
269
350
|
onClick={() => {
|
|
270
351
|
const text = table.shots.map(s => {
|
|
271
352
|
const beat = skeleton?.beats.find(b => b.id === s.beatId)
|
|
272
|
-
return `镜头 ${s.id}(节拍 ${beat?.id ?? s.beatId} · ${s.shot} · ${s.camera} · ${s.duration}s)\n画面:${s.visual}\n台词:${s.line !== '' ? s.line : '(无)'}\n音效:${s.sound !== '' ? s.sound : '(无)'}\n光效:${s.light !== '' ? s.light : '(无)'}\n承接:${s.prevState} → ${s.nextState}`
|
|
353
|
+
return `镜头 ${s.id}(节拍 ${beat?.id ?? s.beatId} · ${sizeZh(s.shot)} · ${cameraZh(s.camera)} · ${s.duration}s)\n出场:${s.characters !== undefined && s.characters.length > 0 ? s.characters.join('、') : '(未标注)'}\n定妆:${s.mangaRoleIds !== undefined && s.mangaRoleIds.length > 0 ? bindingNames(s.mangaRoleIds) : '(未绑定漫剧卡)'}\n画面:${s.visual}\n台词:${s.line !== '' ? s.line : '(无)'}\n音效:${s.sound !== '' ? s.sound : '(无)'}\n光效:${lightZh(s.light) !== '' ? lightZh(s.light) : '(无)'}\n承接:${s.prevState} → ${s.nextState}`
|
|
273
354
|
}).join('\n\n')
|
|
274
355
|
void navigator.clipboard?.writeText(`第${table.chapterNo}章分镜表\n\n` + text)
|
|
275
356
|
}}
|
|
@@ -277,6 +358,12 @@ export function StoryboardTab({
|
|
|
277
358
|
📋 复制分镜表
|
|
278
359
|
</button>
|
|
279
360
|
<span className={css.meta}>按骨架节拍展开 · 镜头间状态连续</span>
|
|
361
|
+
{table.characters !== undefined && table.characters.length > 0 && (
|
|
362
|
+
<span className={css.meta}>👥 出场角色:{table.characters.join('、')}</span>
|
|
363
|
+
)}
|
|
364
|
+
{table.mangaRoleIds !== undefined && table.mangaRoleIds.length > 0 && (
|
|
365
|
+
<span className={css.meta}>🎨 定妆绑定:{bindingNames(table.mangaRoleIds)}</span>
|
|
366
|
+
)}
|
|
280
367
|
{table.usedScenes !== undefined && table.usedScenes.length > 0 && (
|
|
281
368
|
<span className={css.meta}>🏞️ 使用场景:{table.usedScenes.join('、')}</span>
|
|
282
369
|
)}
|
|
@@ -291,7 +378,7 @@ export function StoryboardTab({
|
|
|
291
378
|
<button
|
|
292
379
|
type="button"
|
|
293
380
|
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
294
|
-
onClick={() => {
|
|
381
|
+
onClick={() => { if (prompts === null) void generatePrompts(); onGoStep?.(3) }}
|
|
295
382
|
>
|
|
296
383
|
🎬 下一步:生成视频提示词
|
|
297
384
|
</button>
|
|
@@ -299,18 +386,24 @@ export function StoryboardTab({
|
|
|
299
386
|
{table.shots.map(s => {
|
|
300
387
|
const beat = skeleton?.beats.find(b => b.id === s.beatId)
|
|
301
388
|
return (
|
|
302
|
-
<div key={s.id} style={{ display: 'flex', flexDirection: 'column', gap:
|
|
389
|
+
<div key={s.id} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)' }}>
|
|
303
390
|
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
304
391
|
<b>镜头 {s.id}</b>
|
|
305
|
-
<span className={css.badge}>{s.shot}</span>
|
|
306
|
-
<span className={css.meta}>{s.camera} · {s.duration}s</span>
|
|
307
|
-
{beat !== undefined && <span className={css.meta}>节拍 {beat.id}「{beat.function}」</span>}
|
|
392
|
+
<span className={css.badge}>{sizeZh(s.shot)}</span>
|
|
393
|
+
<span className={css.meta}>{cameraZh(s.camera)}{compoZh(s.composition) !== '' ? ' · ' + compoZh(s.composition) : ''} · {s.duration}s</span>
|
|
394
|
+
{beat !== undefined && <span className={css.meta}>节拍 {beat.id}「{functionZh(beat.function)}」</span>}
|
|
395
|
+
{s.characters !== undefined && s.characters.length > 0 && (
|
|
396
|
+
<span className={css.meta}>👥 {s.characters.join('、')}</span>
|
|
397
|
+
)}
|
|
398
|
+
{s.mangaRoleIds !== undefined && s.mangaRoleIds.length > 0 && (
|
|
399
|
+
<span className={css.badge + ' ' + css.badgeDone}>🎨 定妆 {bindingNames(s.mangaRoleIds)}</span>
|
|
400
|
+
)}
|
|
308
401
|
</div>
|
|
309
402
|
<span>🎞️ {s.visual}</span>
|
|
310
403
|
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
311
404
|
<span className={css.meta}>💬 {s.line !== '' ? s.line : '(无台词)'}</span>
|
|
312
405
|
<span className={css.meta}>🔊 {s.sound !== '' ? s.sound : '(无音效)'}</span>
|
|
313
|
-
<span className={css.meta}>💡 {s.light !== '' ? s.light : '(无光效)'}</span>
|
|
406
|
+
<span className={css.meta}>💡 {lightZh(s.light) !== '' ? lightZh(s.light) : '(无光效)'}</span>
|
|
314
407
|
</div>
|
|
315
408
|
<span className={css.meta}>承接:{s.prevState} → {s.nextState}</span>
|
|
316
409
|
</div>
|
|
@@ -320,7 +413,7 @@ export function StoryboardTab({
|
|
|
320
413
|
)}
|
|
321
414
|
|
|
322
415
|
{step === 3 && prompts !== null && (
|
|
323
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 10 }}>
|
|
416
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', marginTop: 'var(--nf-space-10)' }}>
|
|
324
417
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
325
418
|
<b>🎬 视频提示词({prompts.length} 个镜头)</b>
|
|
326
419
|
<button
|
|
@@ -335,7 +428,7 @@ export function StoryboardTab({
|
|
|
335
428
|
type="button"
|
|
336
429
|
className={`${css.button} ${css.buttonSmall}`}
|
|
337
430
|
onClick={() => {
|
|
338
|
-
const text = prompts.map(x => `【镜头 ${x.shotId}】${x.text}`).join('\n\n')
|
|
431
|
+
const text = prompts.map(x => `【镜头 ${x.shotId}】${x.mangaRoleIds !== undefined && x.mangaRoleIds.length > 0 ? '定妆:' + bindingNames(x.mangaRoleIds) + ' ' : ''}${x.text}`).join('\n\n')
|
|
339
432
|
void navigator.clipboard?.writeText(`第${table?.chapterNo ?? ''}章视频提示词\n\n` + text)
|
|
340
433
|
}}
|
|
341
434
|
>
|
|
@@ -343,10 +436,13 @@ export function StoryboardTab({
|
|
|
343
436
|
</button>
|
|
344
437
|
</div>
|
|
345
438
|
{prompts.map(x => (
|
|
346
|
-
<div key={x.shotId} style={{ display: 'flex', flexDirection: 'column', gap:
|
|
439
|
+
<div key={x.shotId} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)' }}>
|
|
347
440
|
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
348
441
|
<b>镜头 {x.shotId}</b>
|
|
349
442
|
<span className={css.meta}>即梦/Seedance 提示词</span>
|
|
443
|
+
{x.mangaRoleIds !== undefined && x.mangaRoleIds.length > 0 && (
|
|
444
|
+
<span className={css.badge + ' ' + css.badgeDone}>🎨 定妆 {bindingNames(x.mangaRoleIds)}</span>
|
|
445
|
+
)}
|
|
350
446
|
<div style={{ flex: 1 }} />
|
|
351
447
|
<button
|
|
352
448
|
type="button"
|
|
@@ -360,7 +456,7 @@ export function StoryboardTab({
|
|
|
360
456
|
{copied === x.shotId ? '✅ 已复制' : '📋 复制'}
|
|
361
457
|
</button>
|
|
362
458
|
</div>
|
|
363
|
-
<span style={{ fontSize:
|
|
459
|
+
<span style={{ fontSize: 'var(--nf-fs-14)', lineHeight: 1.7 }}>{x.text}</span>
|
|
364
460
|
</div>
|
|
365
461
|
))}
|
|
366
462
|
</div>
|
|
@@ -33,9 +33,9 @@ export function StyleCard({
|
|
|
33
33
|
<div
|
|
34
34
|
onClick={onClick}
|
|
35
35
|
style={{
|
|
36
|
-
display: 'flex', flexDirection: 'column', gap: 6,
|
|
36
|
+
display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)',
|
|
37
37
|
border: `1px solid ${selected === true ? 'var(--nf-accent)' : 'var(--nf-border)'}`,
|
|
38
|
-
borderRadius: 14, padding: 10, background: 'var(--nf-bg-raise)',
|
|
38
|
+
borderRadius: 'var(--nf-radius-14)', padding: 'var(--nf-space-10)', background: 'var(--nf-bg-raise)',
|
|
39
39
|
cursor: onClick !== undefined ? 'pointer' : 'default',
|
|
40
40
|
boxShadow: selected === true ? '0 0 0 2px var(--nf-accent-soft)' : 'none',
|
|
41
41
|
}}
|
|
@@ -43,12 +43,12 @@ export function StyleCard({
|
|
|
43
43
|
{/* 效果图(竖版 3:4,服务端缩略图 webp;无图回退占位) */}
|
|
44
44
|
<div
|
|
45
45
|
style={{
|
|
46
|
-
aspectRatio: '3 / 4', borderRadius: 10,
|
|
46
|
+
aspectRatio: '3 / 4', borderRadius: 'var(--nf-radius-10)',
|
|
47
47
|
position: 'relative', overflow: 'hidden',
|
|
48
48
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
49
49
|
background: 'linear-gradient(145deg, var(--nf-bg-inset), color-mix(in srgb, var(--nf-accent) 12%, var(--nf-bg-inset)))',
|
|
50
50
|
border: '1px solid var(--nf-border)',
|
|
51
|
-
color: 'var(--nf-text-3)', fontSize: 12, textAlign: 'center',
|
|
51
|
+
color: 'var(--nf-text-3)', fontSize: 'var(--nf-fs-12)', textAlign: 'center',
|
|
52
52
|
}}
|
|
53
53
|
>
|
|
54
54
|
<img
|
|
@@ -60,22 +60,22 @@ export function StyleCard({
|
|
|
60
60
|
/>
|
|
61
61
|
🖼️ 效果图待生成
|
|
62
62
|
{selected === true && (
|
|
63
|
-
<span style={{ position: 'absolute', top: 6, right: 6, background: 'var(--nf-accent)', color: '#fff', borderRadius: 999, padding: '
|
|
63
|
+
<span style={{ position: 'absolute', top: 6, right: 6, background: 'var(--nf-accent)', color: '#fff', borderRadius: 'var(--nf-radius-999)', padding: 'var(--nf-space-2) var(--nf-space-8)', fontSize: 'var(--nf-fs-12)', fontWeight: 700 }}>✓</span>
|
|
64
64
|
)}
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
|
68
|
-
<b style={{ fontSize:
|
|
69
|
-
<span style={{ fontSize:
|
|
70
|
-
{style.stackable === true && <span style={{ fontSize:
|
|
67
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
|
|
68
|
+
<b style={{ fontSize: 'var(--nf-fs-14)' }}>{style.name}</b>
|
|
69
|
+
<span style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-text-3)' }}>{cat?.icon} {style.category.toUpperCase()}</span>
|
|
70
|
+
{style.stackable === true && <span style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-info)', border: '1px solid var(--nf-info)', borderRadius: 'var(--nf-radius-999)', padding: '0 var(--nf-space-6)' }}>叠加滤镜</span>}
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
|
-
<span style={{ fontSize:
|
|
73
|
+
<span style={{ fontSize: 'var(--nf-fs-12)', lineHeight: 1.5, color: 'var(--nf-text-2)' }}>{style.traits}</span>
|
|
74
74
|
|
|
75
75
|
<div
|
|
76
76
|
style={{
|
|
77
|
-
fontSize:
|
|
78
|
-
borderTop: '1px solid var(--nf-border)', paddingTop:
|
|
77
|
+
fontSize: 'var(--nf-fs-12)', lineHeight: 1.5, color: 'var(--nf-text-3)',
|
|
78
|
+
borderTop: '1px solid var(--nf-border)', paddingTop: 'var(--nf-space-6)',
|
|
79
79
|
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
|
80
80
|
cursor: 'copy',
|
|
81
81
|
}}
|
|
@@ -84,7 +84,7 @@ export function StyleCard({
|
|
|
84
84
|
>
|
|
85
85
|
关键词:{style.keywords}
|
|
86
86
|
</div>
|
|
87
|
-
{copied === style.id && <span style={{ color: 'var(--nf-ok, #46a758)', fontSize:
|
|
87
|
+
{copied === style.id && <span style={{ color: 'var(--nf-ok, #46a758)', fontSize: 'var(--nf-fs-12)' }}>✅ 已复制</span>}
|
|
88
88
|
</div>
|
|
89
89
|
)
|
|
90
90
|
}
|
|
@@ -30,7 +30,7 @@ export function StyleGallery() {
|
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
32
|
{/* 分类标签 */}
|
|
33
|
-
<div className={css.row} style={{ gap: 8, flexWrap: 'wrap' }}>
|
|
33
|
+
<div className={css.row} style={{ gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
34
34
|
{STYLE_CATEGORIES.map(cat => (
|
|
35
35
|
<button
|
|
36
36
|
key={cat.id}
|
|
@@ -48,7 +48,7 @@ export function StyleGallery() {
|
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
{/* 风格卡片网格 */}
|
|
51
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 12, marginTop: 6 }}>
|
|
51
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 'var(--nf-space-12)', marginTop: 'var(--nf-space-6)' }}>
|
|
52
52
|
{list.map(style => (
|
|
53
53
|
<StyleCard key={style.id} style={style} />
|
|
54
54
|
))}
|
|
@@ -28,7 +28,7 @@ function EditableRow({
|
|
|
28
28
|
detailPlaceholder: string
|
|
29
29
|
}) {
|
|
30
30
|
return (
|
|
31
|
-
<div style={{ display: 'flex', gap: 6, alignItems: 'flex-start' }}>
|
|
31
|
+
<div style={{ display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'flex-start' }}>
|
|
32
32
|
<input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
|
|
33
33
|
<input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
|
|
34
34
|
<button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
|
|
@@ -89,7 +89,7 @@ export function WorldTab({
|
|
|
89
89
|
const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
|
|
90
90
|
|
|
91
91
|
return (
|
|
92
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
92
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)' }}>
|
|
93
93
|
{error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
|
|
94
94
|
|
|
95
95
|
<div className={css.card}>
|
|
@@ -120,7 +120,7 @@ export function WorldTab({
|
|
|
120
120
|
{draft.realms.length === 0 ? (
|
|
121
121
|
<span className={css.meta}>暂无境界体系 — 点击 ✨AI 提炼 或手动添加(由低到高)。</span>
|
|
122
122
|
) : (
|
|
123
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
123
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
124
124
|
{draft.realms.map((realm, i) => (
|
|
125
125
|
<EditableRow
|
|
126
126
|
key={i}
|
|
@@ -148,7 +148,7 @@ export function WorldTab({
|
|
|
148
148
|
{draft.regions.length === 0 ? (
|
|
149
149
|
<span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
|
|
150
150
|
) : (
|
|
151
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
151
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
152
152
|
{draft.regions.map((region, i) => (
|
|
153
153
|
<EditableRow
|
|
154
154
|
key={i}
|
|
@@ -176,7 +176,7 @@ export function WorldTab({
|
|
|
176
176
|
{draft.factions.length === 0 ? (
|
|
177
177
|
<span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
|
|
178
178
|
) : (
|
|
179
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
179
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
180
180
|
{draft.factions.map((faction, i) => (
|
|
181
181
|
<EditableRow
|
|
182
182
|
key={i}
|