@waterwx/dsh-novel-forge 1.7.0 → 1.7.2
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/lib/client.js +2537 -1858
- package/lib/client.js.map +1 -1
- package/lib/index.js +657 -45
- 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 +124 -0
- package/package.json +2 -1
- 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 +10 -10
- package/src/client/panel/FlowGuide.tsx +83 -0
- package/src/client/panel/MangaRoleLibrary.tsx +810 -0
- package/src/client/panel/MangaWorkspace.tsx +359 -297
- package/src/client/panel/NovelPanel.tsx +107 -107
- package/src/client/panel/ReaderView.tsx +4 -4
- package/src/client/panel/RunPanel.tsx +8 -8
- package/src/client/panel/SceneLibrary.tsx +68 -67
- package/src/client/panel/StoryboardTab.tsx +105 -41
- 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 +482 -51
- package/src/protocol.ts +125 -0
- package/src/routes.ts +203 -0
- package/src/client/panel/RoleVisualPanel.tsx +0 -485
|
@@ -1,297 +1,359 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import type {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<div className={css.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 漫剧工作台(方案X:9 步页容器):
|
|
3
|
+
* 顶部常驻:方案切换器 + 全流程步骤条(唯一导航);
|
|
4
|
+
* 主体按步骤渲染独立页面:①创建方案 ②视觉规则 ③剧情骨架 ④分镜表 ⑤导入角色 ⑥角色定妆 ⑦视频提示词 ⑧场景库 ⑨导出使用。
|
|
5
|
+
* 每步页面只显示自己的内容;前置不足显示明确提示,不静默降级、不夹带别区内容。
|
|
6
|
+
*/
|
|
7
|
+
import { useMemo, useState } from 'react'
|
|
8
|
+
import type { NovelApi } from '../api.ts'
|
|
9
|
+
import type { ChapterPlan, ImageModelConfig, MangaPlan, ProjectState } from '../../protocol.ts'
|
|
10
|
+
import { STYLE_CATEGORIES, STYLE_LIBRARY, findStyle, stylesByCategory, type StyleCategory } from '../../style-library.ts'
|
|
11
|
+
import { StyleCard } from './StyleCard.tsx'
|
|
12
|
+
import { StoryboardTab } from './StoryboardTab.tsx'
|
|
13
|
+
import { MangaRoleLibrary } from './MangaRoleLibrary.tsx'
|
|
14
|
+
import { SceneLibrary } from './SceneLibrary.tsx'
|
|
15
|
+
import { FlowGuide, type FlowTarget } from './FlowGuide.tsx'
|
|
16
|
+
import css from './panel.module.css'
|
|
17
|
+
|
|
18
|
+
/** 步骤页编号:1建方案 2视觉规则 3骨架 4分镜表 5导入 6定妆 7提示词 8场景 9导出。 */
|
|
19
|
+
type StepView = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
|
|
20
|
+
|
|
21
|
+
const FLOW_TARGET_TO_VIEW: Record<FlowTarget, StepView> = {
|
|
22
|
+
plan: 1,
|
|
23
|
+
rules: 2,
|
|
24
|
+
skeleton: 3,
|
|
25
|
+
table: 4,
|
|
26
|
+
import: 5,
|
|
27
|
+
makeup: 6,
|
|
28
|
+
prompts: 7,
|
|
29
|
+
scenes: 8,
|
|
30
|
+
export: 9,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function MangaWorkspace({
|
|
34
|
+
api,
|
|
35
|
+
project,
|
|
36
|
+
chapters,
|
|
37
|
+
onProjectChanged,
|
|
38
|
+
imageApiEnabled,
|
|
39
|
+
imageModels,
|
|
40
|
+
onProgress,
|
|
41
|
+
}: {
|
|
42
|
+
api: NovelApi
|
|
43
|
+
project: ProjectState | null
|
|
44
|
+
chapters: ChapterPlan[]
|
|
45
|
+
/** 方案/资产变更已持久化后触发(刷新项目)。 */
|
|
46
|
+
onProjectChanged?: () => void | Promise<void>
|
|
47
|
+
/** 是否启用生图(漫剧卡出定妆图)。 */
|
|
48
|
+
imageApiEnabled?: boolean
|
|
49
|
+
/** 生图模型库(出定妆图时可选择模型)。 */
|
|
50
|
+
imageModels?: ImageModelConfig[]
|
|
51
|
+
/** 上报到「AI进度」控制台(漫剧工作台内所有 LLM/方案操作)。 */
|
|
52
|
+
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
53
|
+
}) {
|
|
54
|
+
const plans = useMemo(() => project?.mangaPlans ?? [], [project?.mangaPlans])
|
|
55
|
+
const activePlan = plans.find(p => p.active) ?? null
|
|
56
|
+
|
|
57
|
+
const [view, setView] = useState<StepView>(3)
|
|
58
|
+
const [busy, setBusy] = useState(false)
|
|
59
|
+
const [error, setError] = useState('')
|
|
60
|
+
|
|
61
|
+
// 风格选择表单(第①步页)
|
|
62
|
+
const [cat, setCat] = useState<StyleCategory>('3d')
|
|
63
|
+
const [selStyle, setSelStyle] = useState('')
|
|
64
|
+
const [selFilter, setSelFilter] = useState('')
|
|
65
|
+
const [planName, setPlanName] = useState('')
|
|
66
|
+
|
|
67
|
+
// 无方案时强制停在创建方案页。
|
|
68
|
+
const effectiveView: StepView = plans.length === 0 ? 1 : view
|
|
69
|
+
|
|
70
|
+
const baseStyles = useMemo(() => STYLE_LIBRARY.filter(s => s.stackable !== true).sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0)), [])
|
|
71
|
+
const filterStyles = useMemo(() => STYLE_LIBRARY.filter(s => s.stackable === true), [])
|
|
72
|
+
const activeStyle = activePlan !== null ? findStyle(activePlan.styleId) : undefined
|
|
73
|
+
const activeFilter = activePlan?.filterId !== undefined ? findStyle(activePlan.filterId) : undefined
|
|
74
|
+
|
|
75
|
+
const refresh = async (): Promise<void> => { await onProjectChanged?.() }
|
|
76
|
+
|
|
77
|
+
const mutate = async (req: import('../../protocol.ts').MangaPlansRequest): Promise<boolean> => {
|
|
78
|
+
const opLabel = req.op === 'create' ? '创建漫剧方案「' + (req.name ?? '') + '」' : req.op === 'activate' ? '切换漫剧方案' : '删除漫剧方案'
|
|
79
|
+
setBusy(true)
|
|
80
|
+
setError('')
|
|
81
|
+
onProgress?.(opLabel + '…')
|
|
82
|
+
try {
|
|
83
|
+
await api.manhuaPlans(req)
|
|
84
|
+
onProgress?.(opLabel + ' 完成', 'done')
|
|
85
|
+
return true
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const m = (err as Error).message
|
|
88
|
+
setError(m)
|
|
89
|
+
onProgress?.(opLabel + ' 失败:' + m, 'error')
|
|
90
|
+
return false
|
|
91
|
+
} finally {
|
|
92
|
+
setBusy(false)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const createPlan = async (): Promise<void> => {
|
|
97
|
+
const n = planName.trim()
|
|
98
|
+
if (n === '' || selStyle === '') {
|
|
99
|
+
setError('请选择基底风格并填写方案名')
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const ok = await mutate({ op: 'create', name: n, styleId: selStyle, filterId: selFilter !== '' ? selFilter : undefined })
|
|
103
|
+
if (ok) {
|
|
104
|
+
setPlanName('')
|
|
105
|
+
setSelStyle('')
|
|
106
|
+
setSelFilter('')
|
|
107
|
+
await refresh()
|
|
108
|
+
setView(2)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pickStyle = (id: string): void => {
|
|
113
|
+
setSelStyle(id)
|
|
114
|
+
if (planName.trim() === '') {
|
|
115
|
+
const s = findStyle(id)
|
|
116
|
+
setPlanName((project?.bookName ?? '本书') + ' · ' + (s?.name ?? '') + '版')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 流程第②步:从道藏提炼视觉规则(注入所有提示词)。 */
|
|
121
|
+
const extractRules = async (): Promise<void> => {
|
|
122
|
+
setBusy(true)
|
|
123
|
+
setError('')
|
|
124
|
+
onProgress?.('从道藏提炼视觉规则…')
|
|
125
|
+
try {
|
|
126
|
+
const r = await api.visualRules({ op: 'extract' })
|
|
127
|
+
await refresh()
|
|
128
|
+
onProgress?.('已提炼 ' + r.rules.length + ' 条视觉规则(已注入所有提示词)', 'done')
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const m = (err as Error).message
|
|
131
|
+
setError(m)
|
|
132
|
+
onProgress?.('提炼视觉规则失败:' + m, 'error')
|
|
133
|
+
} finally {
|
|
134
|
+
setBusy(false)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 全流程步骤条导航:步骤 → 对应页面。 */
|
|
139
|
+
const navigateFlow = (target: FlowTarget): void => {
|
|
140
|
+
setError('')
|
|
141
|
+
setView(FLOW_TARGET_TO_VIEW[target])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 分镜「下一步」回调:1骨架→3、2分镜表→4、3提示词→7(对应步骤页编号)。 */
|
|
145
|
+
const goStep = (n: 1 | 2 | 3): void => {
|
|
146
|
+
setView(n === 1 ? 3 : n === 2 ? 4 : 7)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (project === null) {
|
|
150
|
+
return <div className={css.card}><span className={css.meta}>请先开书或选择一本书,再进入漫剧工作台。</span></div>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rules = project.visualRules ?? []
|
|
154
|
+
const anchoredCards = (project.mangaRoles ?? []).filter(c => c.status === 'anchored')
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)' }}>
|
|
158
|
+
{/* 顶部常驻:方案切换器 + 全流程步骤条(唯一导航) */}
|
|
159
|
+
<div className={css.card}>
|
|
160
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
161
|
+
<span className={css.cardTitle}>🎬 {activePlan?.name ?? '漫剧工作台'}</span>
|
|
162
|
+
<div className={css.row}>
|
|
163
|
+
<span className={css.badge}>🎨 {activeStyle?.name ?? activePlan?.styleId}</span>
|
|
164
|
+
{activeFilter !== undefined && <span className={css.badge}>+{activeFilter.name}</span>}
|
|
165
|
+
<select
|
|
166
|
+
className={css.input}
|
|
167
|
+
style={{ width: 'auto', padding: 'var(--nf-space-4) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)', borderRadius: 'var(--nf-radius-8)' }}
|
|
168
|
+
value={activePlan?.id ?? ''}
|
|
169
|
+
onChange={async e => { if (await mutate({ op: 'activate', id: e.target.value })) await refresh() }}
|
|
170
|
+
>
|
|
171
|
+
{plans.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
172
|
+
</select>
|
|
173
|
+
<button type="button" className={css.button + ' ' + css.buttonSmall} style={{ minWidth: 84 }} onClick={() => { setSelStyle(''); setPlanName(''); setView(1) }}>+ 新建</button>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
className={css.button + ' ' + css.buttonSmall}
|
|
177
|
+
disabled={busy}
|
|
178
|
+
onClick={async () => {
|
|
179
|
+
if (activePlan === null) return
|
|
180
|
+
if (!window.confirm('删除方案「' + activePlan.name + '」?该方案的分镜产出不受影响(骨架共享)。')) return
|
|
181
|
+
if (await mutate({ op: 'remove', id: activePlan.id })) {
|
|
182
|
+
await refresh()
|
|
183
|
+
setView(1)
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
🗑 删除当前方案
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<FlowGuide project={project} onNavigate={navigateFlow} />
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{error !== '' && <div className={css.importError}>{error}</div>}
|
|
195
|
+
|
|
196
|
+
{/* ① 创建方案页 */}
|
|
197
|
+
{effectiveView === 1 && (
|
|
198
|
+
<div className={css.card}>
|
|
199
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
200
|
+
<span className={css.cardTitle}>① 创建方案 · 选择视觉风格</span>
|
|
201
|
+
{plans.length > 0 && <button type="button" className={css.button} disabled={busy} onClick={() => { setView(3) }}>返回工作台</button>}
|
|
202
|
+
</div>
|
|
203
|
+
<span className={css.meta}>选一个基底风格(影视类可叠加滤镜)并命名。此后所有提示词按此风格生成。</span>
|
|
204
|
+
|
|
205
|
+
<div className={css.row} style={{ gap: 'var(--nf-space-8)', flexWrap: 'wrap', margin: '10px 0' }}>
|
|
206
|
+
{STYLE_CATEGORIES.map(c => (
|
|
207
|
+
<button key={c.id} type="button" className={css.button + (cat === c.id ? ' ' + css.buttonPrimary : '')} style={{ flex: 1, minWidth: 120 }} onClick={() => { setCat(c.id) }}>
|
|
208
|
+
{c.icon} {c.label}({stylesByCategory(c.id).length})
|
|
209
|
+
</button>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
<div className={css.meta}>{STYLE_CATEGORIES.find(c => c.id === cat)?.desc}</div>
|
|
213
|
+
|
|
214
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 'var(--nf-space-12)', margin: '10px 0' }}>
|
|
215
|
+
{stylesByCategory(cat).map(s => (
|
|
216
|
+
<StyleCard key={s.id} style={s} selected={selStyle === s.id} onClick={() => { pickStyle(s.id) }} />
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{filterStyles.length > 0 && (
|
|
221
|
+
<div className={css.field}>
|
|
222
|
+
<label className={css.fieldLabel}>叠加滤镜(可选)</label>
|
|
223
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
224
|
+
<button type="button" className={css.button + ' ' + css.buttonSmall + (selFilter === '' ? ' ' + css.buttonPrimary : '')} onClick={() => { setSelFilter('') }}>无</button>
|
|
225
|
+
{filterStyles.map(s => (
|
|
226
|
+
<button key={s.id} type="button" className={css.button + ' ' + css.buttonSmall + (selFilter === s.id ? ' ' + css.buttonPrimary : '')} onClick={() => { setSelFilter(s.id) }}>{s.name}</button>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
<div className={css.field}>
|
|
233
|
+
<label className={css.fieldLabel}>方案名</label>
|
|
234
|
+
<input className={css.input} value={planName} placeholder="例如:《保质期》3D 皮克斯版" onChange={e => { setPlanName(e.target.value) }} />
|
|
235
|
+
</div>
|
|
236
|
+
<div className={css.row}>
|
|
237
|
+
<button type="button" className={css.button + ' ' + css.buttonPrimary} disabled={busy || selStyle === '' || planName.trim() === ''} onClick={() => { void createPlan() }}>
|
|
238
|
+
{busy ? '创建中…' : '🎬 创建方案'}
|
|
239
|
+
</button>
|
|
240
|
+
<span className={css.meta}>{selStyle !== '' ? '已选:' + (findStyle(selStyle)?.name ?? selStyle) : '点击卡片选择基底风格'}</span>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* ② 视觉规则页 */}
|
|
246
|
+
{effectiveView === 2 && (
|
|
247
|
+
<div className={css.card}>
|
|
248
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
249
|
+
<span className={css.cardTitle}>② 视觉规则</span>
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
className={css.button + ' ' + css.buttonSmall}
|
|
253
|
+
disabled={busy}
|
|
254
|
+
onClick={() => { void extractRules() }}
|
|
255
|
+
title="从道藏提炼 3-6 条视觉世界观规则,自动注入所有生图/生视频提示词"
|
|
256
|
+
>
|
|
257
|
+
{busy ? '提炼中…' : '⚠️ 提炼视觉规则(从道藏)'}
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
<span className={css.meta} style={{ display: 'block', marginTop: 'var(--nf-space-4)' }}>
|
|
261
|
+
AI 从道藏提炼 3-6 条「画面必须遵守」的视觉世界观规则(如:货架上的商品都是活人),自动注入分镜表、角色锚点、视频提示词等所有提示词。
|
|
262
|
+
</span>
|
|
263
|
+
{rules.length > 0 ? (
|
|
264
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-4)', marginTop: 'var(--nf-space-8)' }}>
|
|
265
|
+
<span className={css.meta} style={{ fontSize: 'var(--nf-fs-12)' }}>已内嵌到所有提示词:</span>
|
|
266
|
+
{rules.map((r, i) => (
|
|
267
|
+
<span key={i} style={{ border: '1px solid var(--nf-warn, #b8860b)', borderRadius: 'var(--nf-radius-999)', padding: '0 var(--nf-space-8)', fontSize: 'var(--nf-fs-12)', color: 'var(--nf-warn, #b8860b)' }}>{r}</span>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
) : (
|
|
271
|
+
<span className={css.meta} style={{ marginTop: 'var(--nf-space-4)' }}>还没提炼——点上方按钮生成第一条视觉规则。</span>
|
|
272
|
+
)}
|
|
273
|
+
<div className={css.row} style={{ marginTop: 'var(--nf-space-10)' }}>
|
|
274
|
+
<button type="button" className={css.button + ' ' + css.buttonSmall + ' ' + css.buttonPrimary} onClick={() => { setView(3) }}>下一步:③ 剧情骨架</button>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{/* ③④⑦ 分镜步骤页(同一实例,mode 切换) */}
|
|
280
|
+
{(effectiveView === 3 || effectiveView === 4 || effectiveView === 7) && (
|
|
281
|
+
<StoryboardTab
|
|
282
|
+
api={api}
|
|
283
|
+
project={project}
|
|
284
|
+
chapters={chapters}
|
|
285
|
+
onProjectChanged={onProjectChanged}
|
|
286
|
+
styleId={activePlan?.styleId}
|
|
287
|
+
filterId={activePlan?.filterId}
|
|
288
|
+
mode={effectiveView === 3 ? 'skeleton' : effectiveView === 4 ? 'table' : 'prompts'}
|
|
289
|
+
onGoStep={goStep}
|
|
290
|
+
onProgress={onProgress}
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
{/* ⑤⑥ 角色页(同一实例,focus 切换) */}
|
|
295
|
+
{(effectiveView === 5 || effectiveView === 6) && (
|
|
296
|
+
<MangaRoleLibrary
|
|
297
|
+
api={api}
|
|
298
|
+
project={project}
|
|
299
|
+
refresh={() => refresh()}
|
|
300
|
+
styleId={activePlan?.styleId}
|
|
301
|
+
filterId={activePlan?.filterId}
|
|
302
|
+
imageApiEnabled={imageApiEnabled}
|
|
303
|
+
imageModels={imageModels}
|
|
304
|
+
focus={effectiveView === 5 ? 'import' : 'cards'}
|
|
305
|
+
showCards={effectiveView !== 5}
|
|
306
|
+
onProgress={onProgress}
|
|
307
|
+
/>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{/* ⑧ 场景库页 */}
|
|
311
|
+
{effectiveView === 8 && (
|
|
312
|
+
<SceneLibrary
|
|
313
|
+
api={api}
|
|
314
|
+
project={project}
|
|
315
|
+
refresh={() => refresh()}
|
|
316
|
+
styleId={activePlan?.styleId}
|
|
317
|
+
filterId={activePlan?.filterId}
|
|
318
|
+
onProgress={onProgress}
|
|
319
|
+
/>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* ⑨ 导出使用页 */}
|
|
323
|
+
{effectiveView === 9 && (
|
|
324
|
+
<div className={css.card}>
|
|
325
|
+
<span className={css.cardTitle}>⑨ 导出使用</span>
|
|
326
|
+
<span className={css.meta} style={{ display: 'block', marginTop: 'var(--nf-space-4)' }}>
|
|
327
|
+
把视频提示词复制到外部工具(即梦 / 豆包 / ComfyUI)出图生视频:
|
|
328
|
+
</span>
|
|
329
|
+
<ol style={{ margin: '6px 0 0', paddingLeft: 'var(--nf-space-20)', fontSize: 'var(--nf-fs-12)', lineHeight: 1.9, color: 'var(--nf-text)' }}>
|
|
330
|
+
<li>去「⑦ 视频提示词」页复制——每条已带风格词块、视觉规则与定妆绑定标注</li>
|
|
331
|
+
<li>出图/生视频时,按镜头绑定的漫剧卡取定妆参考图(IP-Adapter 类方案锁脸锁服装)</li>
|
|
332
|
+
<li>场景卡与视觉规则已内嵌在提示词里,无需额外粘贴</li>
|
|
333
|
+
</ol>
|
|
334
|
+
<div className={css.row} style={{ marginTop: 'var(--nf-space-10)', flexWrap: 'wrap' }}>
|
|
335
|
+
<button type="button" className={css.button + ' ' + css.buttonSmall + ' ' + css.buttonPrimary} onClick={() => { setView(7) }}>去复制视频提示词</button>
|
|
336
|
+
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { setView(6) }}>查看定妆卡</button>
|
|
337
|
+
</div>
|
|
338
|
+
{anchoredCards.length > 0 && (
|
|
339
|
+
<div style={{ marginTop: 'var(--nf-space-10)' }}>
|
|
340
|
+
<span className={css.meta}>已定妆角色({anchoredCards.length}):</span>
|
|
341
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-8)', marginTop: 'var(--nf-space-6)' }}>
|
|
342
|
+
{anchoredCards.map(c => (
|
|
343
|
+
<div key={c.id} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', alignItems: 'center', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-6)' }}>
|
|
344
|
+
{c.imageUrl !== undefined ? (
|
|
345
|
+
<img src={c.imageUrl} alt={c.name} style={{ maxHeight: 90, borderRadius: 'var(--nf-radius-6)' }} />
|
|
346
|
+
) : (
|
|
347
|
+
<span style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-text-dim, #888)' }}>(无定妆图)</span>
|
|
348
|
+
)}
|
|
349
|
+
<span style={{ fontSize: 'var(--nf-fs-12)' }}>{c.name}</span>
|
|
350
|
+
</div>
|
|
351
|
+
))}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
}
|