@waterwx/dsh-novel-forge 2.1.1-alpha → 2.1.1

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.
Files changed (253) hide show
  1. package/README.md +368 -472
  2. package/lib/client.js +19578 -21391
  3. package/lib/index.js +4640 -3932
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +0 -6
  6. package/lib/types/assets.d.ts +7 -9
  7. package/lib/types/assistant.d.ts +25 -0
  8. package/lib/types/author-assets.d.ts +1 -3
  9. package/lib/types/bookshelf.d.ts +13 -3
  10. package/lib/types/client/api.d.ts +49 -29
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -2
  13. package/lib/types/client/llmLive.d.ts +12 -14
  14. package/lib/types/client/locales.d.ts +12 -67
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssetsTab.d.ts +1 -1
  19. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  20. package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
  21. package/lib/types/client/panel/AuthorHome.d.ts +12 -12
  22. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  23. package/lib/types/client/panel/CreateBookView.d.ts +1 -1
  24. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
  25. package/lib/types/client/panel/ImportModal.d.ts +1 -1
  26. package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +7 -2
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
  31. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  32. package/lib/types/client/panel/ReaderView.d.ts +1 -1
  33. package/lib/types/client/panel/RunPanel.d.ts +1 -1
  34. package/lib/types/client/panel/SettingsView.d.ts +13 -10
  35. package/lib/types/client/panel/ShelfView.d.ts +11 -2
  36. package/lib/types/client/panel/SubPage.d.ts +27 -0
  37. package/lib/types/client/panel/TensionView.d.ts +18 -0
  38. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  39. package/lib/types/client/panel/WorldTab.d.ts +1 -1
  40. package/lib/types/client/panel/helpers.d.ts +5 -3
  41. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  42. package/lib/types/client/panel/outline-markdown.d.ts +112 -0
  43. package/lib/types/client/panel/status-badge.d.ts +13 -0
  44. package/lib/types/client/panel/storage-parse.d.ts +46 -0
  45. package/lib/types/client/panel/toast.d.ts +2 -0
  46. package/lib/types/client/panel/usePersistentState.d.ts +17 -0
  47. package/lib/types/client/panel/views.d.ts +21 -35
  48. package/lib/types/client/run-status.d.ts +21 -0
  49. package/lib/types/client/sidebar-entry.d.ts +0 -2
  50. package/lib/types/digest-parse.d.ts +29 -0
  51. package/lib/types/docx-core.d.ts +16 -0
  52. package/lib/types/docx.d.ts +2 -3
  53. package/lib/types/engine.d.ts +122 -103
  54. package/lib/types/home.d.ts +11 -0
  55. package/lib/types/index.d.ts +21 -13
  56. package/lib/types/llm-live.d.ts +62 -36
  57. package/lib/types/llm-retry.d.ts +38 -0
  58. package/lib/types/market-radar-scan.d.ts +0 -1
  59. package/lib/types/market-radar-sources.d.ts +1 -1
  60. package/lib/types/migrate-output.d.ts +25 -0
  61. package/lib/types/model-capability.d.ts +119 -0
  62. package/lib/types/model-request.d.ts +36 -0
  63. package/lib/types/output-budget.d.ts +86 -0
  64. package/lib/types/plotline-align.d.ts +35 -0
  65. package/lib/types/prompt-slots.d.ts +57 -0
  66. package/lib/types/prompting.d.ts +2 -27
  67. package/lib/types/protocol.d.ts +479 -595
  68. package/lib/types/revision.d.ts +76 -0
  69. package/lib/types/routes.d.ts +9 -3
  70. package/lib/types/run-selection.d.ts +51 -0
  71. package/lib/types/run.d.ts +31 -0
  72. package/lib/types/settings-persist.d.ts +59 -0
  73. package/lib/types/snapshots.d.ts +46 -0
  74. package/lib/types/stage-contract.d.ts +58 -0
  75. package/lib/types/stream-fallback.d.ts +60 -0
  76. package/lib/types/strip-headings.d.ts +33 -0
  77. package/lib/types/tension.d.ts +53 -0
  78. package/lib/types/timeline.d.ts +37 -0
  79. package/package.json +41 -28
  80. package/assets/styles/arcane-thick-3d.webp +0 -0
  81. package/assets/styles/chinese-ink.webp +0 -0
  82. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  83. package/assets/styles/dark-ghibli.webp +0 -0
  84. package/assets/styles/dark-gothic-3d.webp +0 -0
  85. package/assets/styles/film-photo.webp +0 -0
  86. package/assets/styles/guofeng-heavy.webp +0 -0
  87. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  88. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  89. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  90. package/assets/styles/horror-game-3d.webp +0 -0
  91. package/assets/styles/hyperreal-3d.webp +0 -0
  92. package/assets/styles/japan-cel-dark.webp +0 -0
  93. package/assets/styles/korean-webtoon.webp +0 -0
  94. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  95. package/assets/styles/nextgen-cg.webp +0 -0
  96. package/assets/styles/noir-film.webp +0 -0
  97. package/assets/styles/openworld-survival.webp +0 -0
  98. package/assets/styles/paper-cut-3d.webp +0 -0
  99. package/assets/styles/pixar-adult-3d.webp +0 -0
  100. package/assets/styles/pop-marvel-2d.webp +0 -0
  101. package/assets/styles/sandiao-qban.webp +0 -0
  102. package/assets/styles/shinkai-anime.webp +0 -0
  103. package/assets/styles/tactical-stealth.webp +0 -0
  104. package/assets/styles/urban-lightreal-3d.webp +0 -0
  105. package/assets/styles/vaporwave-3d.webp +0 -0
  106. package/assets/styles/vhs90.webp +0 -0
  107. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  108. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  109. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  110. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  111. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  112. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  113. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  114. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  115. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  116. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  117. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  118. package/lib/types/comic-presets.d.ts +0 -12
  119. package/lib/types/manga-genre-rules.d.ts +0 -22
  120. package/lib/types/prompt-utils.d.ts +0 -35
  121. package/lib/types/review-policy.d.ts +0 -97
  122. package/lib/types/shot-language.d.ts +0 -46
  123. package/lib/types/src/ai-scan.d.ts +0 -36
  124. package/lib/types/src/assets.d.ts +0 -37
  125. package/lib/types/src/assistant.d.ts +0 -45
  126. package/lib/types/src/author-assets.d.ts +0 -25
  127. package/lib/types/src/bookshelf.d.ts +0 -42
  128. package/lib/types/src/client/api.d.ts +0 -234
  129. package/lib/types/src/client/docx.d.ts +0 -15
  130. package/lib/types/src/client/index.d.ts +0 -14
  131. package/lib/types/src/client/locales.d.ts +0 -212
  132. package/lib/types/src/client/mount.d.ts +0 -9
  133. package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
  134. package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
  135. package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
  136. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
  137. package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
  138. package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
  139. package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
  140. package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
  141. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
  142. package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
  143. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
  144. package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
  145. package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
  146. package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
  147. package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
  148. package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
  149. package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
  150. package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
  151. package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
  152. package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
  153. package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
  154. package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
  155. package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
  156. package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
  157. package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
  158. package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
  159. package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
  160. package/lib/types/src/client/panel/controller.d.ts +0 -19
  161. package/lib/types/src/client/panel/helpers.d.ts +0 -18
  162. package/lib/types/src/client/panel/views.d.ts +0 -77
  163. package/lib/types/src/client/sidebar-entry.d.ts +0 -13
  164. package/lib/types/src/client/text.d.ts +0 -9
  165. package/lib/types/src/comic-presets.d.ts +0 -12
  166. package/lib/types/src/docx.d.ts +0 -19
  167. package/lib/types/src/engine.d.ts +0 -402
  168. package/lib/types/src/index.d.ts +0 -79
  169. package/lib/types/src/manga-genre-rules.d.ts +0 -22
  170. package/lib/types/src/novel-context.d.ts +0 -80
  171. package/lib/types/src/prompt-utils.d.ts +0 -35
  172. package/lib/types/src/protocol.d.ts +0 -2176
  173. package/lib/types/src/review-policy.d.ts +0 -97
  174. package/lib/types/src/routes.d.ts +0 -29
  175. package/lib/types/src/run.d.ts +0 -32
  176. package/lib/types/src/shot-language.d.ts +0 -46
  177. package/lib/types/src/story-beat-language.d.ts +0 -35
  178. package/lib/types/src/style-library.d.ts +0 -33
  179. package/lib/types/story-beat-language.d.ts +0 -35
  180. package/lib/types/style-library.d.ts +0 -33
  181. package/src/ai-scan.ts +0 -179
  182. package/src/assets.ts +0 -1774
  183. package/src/assistant.ts +0 -915
  184. package/src/author-assets.ts +0 -147
  185. package/src/bookshelf.ts +0 -167
  186. package/src/client/api.ts +0 -716
  187. package/src/client/css-modules.d.ts +0 -8
  188. package/src/client/docx.ts +0 -69
  189. package/src/client/index.ts +0 -34
  190. package/src/client/llmLive.ts +0 -101
  191. package/src/client/locales.ts +0 -418
  192. package/src/client/mount.tsx +0 -97
  193. package/src/client/mount.tsx._new +0 -97
  194. package/src/client/mount.tsx._nf +0 -97
  195. package/src/client/mount.tsx._tmp +0 -97
  196. package/src/client/panel/AdaptModeView.tsx +0 -535
  197. package/src/client/panel/AssetsTab.tsx +0 -458
  198. package/src/client/panel/AssistantTab.tsx +0 -339
  199. package/src/client/panel/AuthorAssetsView.tsx +0 -208
  200. package/src/client/panel/AuthorHome.tsx +0 -234
  201. package/src/client/panel/BookAnalysisView.tsx +0 -60
  202. package/src/client/panel/BookshelfBar.tsx +0 -170
  203. package/src/client/panel/CreateBookView.tsx +0 -371
  204. package/src/client/panel/DirectorView.tsx +0 -200
  205. package/src/client/panel/FlowGuide.tsx +0 -84
  206. package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
  207. package/src/client/panel/IdeaInspirationView.tsx +0 -71
  208. package/src/client/panel/ImportModal.tsx +0 -303
  209. package/src/client/panel/KnowledgeBaseView.tsx +0 -73
  210. package/src/client/panel/LiveFeedLog.tsx +0 -76
  211. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  212. package/src/client/panel/MangaWorkspace.tsx +0 -715
  213. package/src/client/panel/MarketRadarView.tsx +0 -374
  214. package/src/client/panel/ModelManager.tsx +0 -205
  215. package/src/client/panel/NovelPanel.tsx +0 -4721
  216. package/src/client/panel/ProgressConsole.tsx +0 -63
  217. package/src/client/panel/ProgressHomeView.tsx +0 -87
  218. package/src/client/panel/PropLibrary.tsx +0 -175
  219. package/src/client/panel/ReaderView.tsx +0 -306
  220. package/src/client/panel/ReasoningSection.tsx +0 -31
  221. package/src/client/panel/RunPanel.tsx +0 -204
  222. package/src/client/panel/SceneLibrary.tsx +0 -453
  223. package/src/client/panel/SettingsView.tsx +0 -289
  224. package/src/client/panel/ShelfView.tsx +0 -242
  225. package/src/client/panel/StoryboardTab.tsx +0 -472
  226. package/src/client/panel/StyleCard.tsx +0 -90
  227. package/src/client/panel/StyleGallery.tsx +0 -58
  228. package/src/client/panel/WorldTab.tsx +0 -202
  229. package/src/client/panel/controller.ts +0 -45
  230. package/src/client/panel/helpers.ts +0 -67
  231. package/src/client/panel/panel.module.css +0 -4359
  232. package/src/client/panel/views.tsx +0 -314
  233. package/src/client/sidebar-entry.ts +0 -122
  234. package/src/client/text.ts +0 -28
  235. package/src/comic-presets.ts +0 -67
  236. package/src/docx.ts +0 -83
  237. package/src/engine.ts +0 -5636
  238. package/src/global-assets.ts +0 -59
  239. package/src/index.ts +0 -346
  240. package/src/llm-live.ts +0 -68
  241. package/src/manga-genre-rules.ts +0 -87
  242. package/src/market-radar-scan.ts +0 -41
  243. package/src/market-radar-sources.ts +0 -173
  244. package/src/novel-context.ts +0 -287
  245. package/src/prompt-utils.ts +0 -73
  246. package/src/prompting.ts +0 -159
  247. package/src/protocol.ts +0 -2565
  248. package/src/review-policy.ts +0 -161
  249. package/src/routes.ts +0 -3939
  250. package/src/run.ts +0 -338
  251. package/src/shot-language.ts +0 -181
  252. package/src/story-beat-language.ts +0 -130
  253. package/src/style-library.ts +0 -84
@@ -1,472 +0,0 @@
1
- /**
2
- * 分镜工作台:① 编剧级剧情骨架 → ② 导演级分镜表(镜头级)→ ③ 视频提示词(后续版本)。
3
- * 定位:辅助人工——每级可重新生成、可复制,产出可导出。
4
- */
5
- import { useEffect, useMemo, useRef, useState } from 'react'
6
- import type { NovelApi } from '../api.ts'
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'
14
- import css from './panel.module.css'
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
-
27
- export function StoryboardTab({
28
- api,
29
- project,
30
- chapters,
31
- onProjectChanged,
32
- styleId,
33
- filterId,
34
- mode,
35
- onGoStep,
36
- onProgress,
37
- }: {
38
- api: NovelApi
39
- project: ProjectState | null
40
- chapters: ChapterPlan[]
41
- /** 生成成功且已持久化后触发(刷新项目,切章/重进可恢复)。 */
42
- onProjectChanged?: () => void | Promise<void>
43
- /** 漫剧基底风格 id(画面措辞随风格)。 */
44
- styleId?: string
45
- /** 可选滤镜风格 id。 */
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
51
- /** 上报到「AI进度」控制台(分镜三步生成)。 */
52
- onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
53
- }) {
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('、')
62
- const [chapterNo, setChapterNo] = useState<number | null>(written[0]?.no ?? null)
63
- const [busy, setBusy] = useState(false)
64
- const [error, setError] = useState('')
65
- const [skeleton, setSkeleton] = useState<StoryboardSkeleton | null>(null)
66
- const [table, setTable] = useState<StoryboardTable | null>(null)
67
- const [tableBusy, setTableBusy] = useState(false)
68
- const [prompts, setPrompts] = useState<StoryboardPrompt[] | null>(null)
69
- const [promptsBusy, setPromptsBusy] = useState(false)
70
- const [copied, setCopied] = useState('')
71
- const [expandedShots, setExpandedShots] = useState<Set<string>>(new Set())
72
- const [promptsExpanded, setPromptsExpanded] = useState(false)
73
- /** 重新生成期间抑制「从持久化恢复旧缓存」的回填(防旧产物复活)。 */
74
- const suppressRestoreRef = useRef(false)
75
- const markRestoreSuppressed = (): void => {
76
- suppressRestoreRef.current = true
77
- window.setTimeout(() => { suppressRestoreRef.current = false }, 500)
78
- }
79
-
80
- // 章节列表变化时保持选中有效章节
81
- useEffect(() => {
82
- if (chapterNo === null || !written.some(c => c.no === chapterNo)) {
83
- setChapterNo(written[0]?.no ?? null)
84
- setSkeleton(null)
85
- setTable(null)
86
- setPrompts(null)
87
- }
88
- }, [written, chapterNo])
89
-
90
- // 从项目持久化恢复:切章节 / 重新进入本页时,读回已保存的骨架与分镜表(本地已有则不覆盖)。
91
- // 重新生成期间(markRestoreSuppressed)跳过恢复,避免旧的下游产物(分镜表/提示词)被拉回来。
92
- useEffect(() => {
93
- if (chapterNo === null) return
94
- if (suppressRestoreRef.current === true) return
95
- const entry = (project?.storyboards ?? []).find(e => e.chapterNo === chapterNo)
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
- })
113
- setPrompts(prev => prev ?? entry?.prompts ?? null)
114
- }, [chapterNo, project?.storyboards])
115
-
116
- const generate = async (chain: boolean): Promise<void> => {
117
- if (chapterNo === null) return
118
- setBusy(true)
119
- setError('')
120
- setSkeleton(null)
121
- setTable(null)
122
- setPrompts(null)
123
- setStepState(1)
124
- onProgress?.('第' + chapterNo + '章 剧情骨架生成中…')
125
- try {
126
- const result = await api.storyboardSkeleton(chapterNo)
127
- setSkeleton(result.skeleton)
128
- markRestoreSuppressed()
129
- void onProjectChanged?.()
130
- onProgress?.('第' + chapterNo + '章 剧情骨架已生成(' + result.skeleton.beats.length + ' 个节拍)', 'done')
131
- // 重新生成:骨架已变、下游已清空,级联自动进入第 ② 步并重算分镜表。
132
- if (chain) await generateTable(result.skeleton)
133
- } catch (err) {
134
- const m = (err as Error).message
135
- setError(m)
136
- onProgress?.('第' + chapterNo + '章 剧情骨架生成失败:' + m, 'error')
137
- } finally {
138
- setBusy(false)
139
- }
140
- }
141
-
142
- /** ② 生成分镜表(骨架 → 镜头级)。forceSkeleton 供①级联重算时传入新骨架。 */
143
- const generateTable = async (forceSkeleton?: StoryboardSkeleton): Promise<void> => {
144
- if (chapterNo === null) return
145
- const sk = forceSkeleton ?? skeleton
146
- if (sk === null) return
147
- setStepState(2)
148
- setTableBusy(true)
149
- setError('')
150
- setTable(null)
151
- setPrompts(null)
152
- onProgress?.('第' + chapterNo + '章 分镜表生成中…')
153
- try {
154
- const result = await api.storyboardTable(chapterNo, sk, styleId, filterId)
155
- setTable(result.table)
156
- markRestoreSuppressed()
157
- void onProjectChanged?.()
158
- onProgress?.('第' + chapterNo + '章 分镜表已生成(' + result.table.shots.length + ' 个镜头)', 'done')
159
- } catch (err) {
160
- const m = (err as Error).message
161
- setError(m)
162
- onProgress?.('第' + chapterNo + '章 分镜表生成失败:' + m, 'error')
163
- } finally {
164
- setTableBusy(false)
165
- }
166
- }
167
-
168
- /** ③ 生成视频提示词(分镜表 → 即梦可粘贴)。 */
169
- const generatePrompts = async (): Promise<void> => {
170
- if (chapterNo === null || table === null) return
171
- setStepState(3)
172
- setPromptsBusy(true)
173
- setError('')
174
- setPrompts(null)
175
- onProgress?.('第' + chapterNo + '章 视频提示词生成中…')
176
- try {
177
- const result = await api.storyboardPrompts(chapterNo, table, styleId, filterId)
178
- setPrompts(result.prompts)
179
- markRestoreSuppressed()
180
- void onProjectChanged?.()
181
- onProgress?.('第' + chapterNo + '章 视频提示词已生成(' + result.prompts.length + ' 条)', 'done')
182
- } catch (err) {
183
- const m = (err as Error).message
184
- setError(m)
185
- onProgress?.('第' + chapterNo + '章 视频提示词生成失败:' + m, 'error')
186
- } finally {
187
- setPromptsBusy(false)
188
- }
189
- }
190
-
191
- const [stepState, setStepState] = useState(1)
192
-
193
- if (project === null) {
194
- return <div className={css.card}><span className={css.meta}>请先开书或选择一本书,再进入分镜工作台。</span></div>
195
- }
196
- const maxStep = prompts !== null ? 3 : table !== null ? 2 : skeleton !== null ? 1 : 0
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))
200
-
201
- return (
202
- <div className={css.card}>
203
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
204
- <span className={css.cardTitle}>🎬 分镜工作台</span>
205
- <span className={css.meta}>三步向导 · {styleId !== undefined ? '按当前方案风格生成' : '未选方案风格'}</span>
206
- </div>
207
-
208
- <span className={css.meta} style={{ display: 'block', marginBottom: 'var(--nf-space-6)' }}>
209
- {step === 1
210
- ? '这步:从章节正文提炼剧情骨架(弧线/节拍/出场角色)· 前置:已写章节 · 下一步:生成分镜表'
211
- : step === 2
212
- ? '这步:骨架展开为镜头级分镜表(景别/机位/台词/每镜头角色+定妆绑定)· 前置:剧情骨架 · 下一步:视频提示词'
213
- : '这步:分镜表 → 即梦可粘贴视频提示词(带风格词块与定妆绑定)· 前置:分镜表(建议先完成角色定妆)· 下一步:复制到即梦/豆包'}
214
- </span>
215
-
216
- <div className={css.row} style={{ alignItems: 'flex-end', flexWrap: 'wrap' }}>
217
- <div className={css.field} style={{ flex: 1, minWidth: 200 }}>
218
- <label className={css.fieldLabel}>选择章节</label>
219
- <select
220
- className={css.input}
221
- value={chapterNo ?? ''}
222
- onChange={e => { setChapterNo(Number(e.target.value)); setError('') }}
223
- >
224
- {written.length === 0 && <option value="">(没有已写章节)</option>}
225
- {written.map(c => (
226
- <option key={c.no} value={c.no}>第{c.no}章 {c.title}</option>
227
- ))}
228
- </select>
229
- </div>
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
- </>
247
- )}
248
- </div>
249
-
250
- {error !== '' && <div className={css.importError}>{error}</div>}
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
-
281
- {step === 1 && (skeleton === null ? (
282
- <div className={css.meta} style={{ marginTop: 'var(--nf-space-8)' }}>
283
- 生成后这里显示本章剧情骨架:弧线 + 节拍链(事件 / 情绪走向 / 叙事功能 / 因果)。
284
- 骨架是「完整剧情」的根——确认骨架没问题后,进入下一步展开为分镜表。
285
- </div>
286
- ) : (
287
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', marginTop: 'var(--nf-space-8)' }}>
288
- <div className={css.importPreview}>
289
- <span>📖 本章弧线:{skeleton.arc}</span>
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
- )}
299
- {skeleton.beats.map((b, i) => (
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)' }}>
301
- <div className={css.row} style={{ flexWrap: 'wrap' }}>
302
- <b>节拍 {i + 1}</b>
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>
305
- </div>
306
- <span>{b.event}</span>
307
- {b.cause !== undefined && <span className={css.meta}>承接:{b.cause}</span>}
308
- </div>
309
- ))}
310
- <div className={css.row}>
311
- <button
312
- type="button"
313
- className={`${css.button} ${css.buttonSmall}`}
314
- onClick={() => {
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')
316
- void navigator.clipboard?.writeText(text)
317
- }}
318
- >
319
- 📋 复制骨架
320
- </button>
321
- <button
322
- type="button"
323
- className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
324
- onClick={() => { if (table === null) void generateTable(); onGoStep?.(2) }}
325
- >
326
- 🎬 下一步:生成分镜表
327
- </button>
328
- <span className={css.meta}>共 {skeleton.beats.length} 个节拍 · 骨架可重新生成(后续版本支持直接编辑)</span>
329
- </div>
330
- </div>
331
- ))}
332
-
333
- {step === 2 && table !== null && (
334
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)', marginTop: 'var(--nf-space-10)' }}>
335
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
336
- <b>🎬 分镜表({table.shots.length} 个镜头)</b>
337
- <button
338
- type="button"
339
- className={`${css.button} ${css.buttonSmall}`}
340
- onClick={() => {
341
- if (expandedShots.size > 0) setExpandedShots(new Set())
342
- else setExpandedShots(new Set(table.shots.map(s => s.id)))
343
- }}
344
- >
345
- {expandedShots.size > 0 ? '▴ 收起全部' : '▾ 展开全部'}
346
- </button>
347
- <button
348
- type="button"
349
- className={`${css.button} ${css.buttonSmall}`}
350
- onClick={() => {
351
- const text = table.shots.map(s => {
352
- const beat = skeleton?.beats.find(b => b.id === s.beatId)
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}`
354
- }).join('\n\n')
355
- void navigator.clipboard?.writeText(`第${table.chapterNo}章分镜表\n\n` + text)
356
- }}
357
- >
358
- 📋 复制分镜表
359
- </button>
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
- )}
367
- {table.usedScenes !== undefined && table.usedScenes.length > 0 && (
368
- <span className={css.meta}>🏞️ 使用场景:{table.usedScenes.join('、')}</span>
369
- )}
370
- <button
371
- type="button"
372
- className={`${css.button} ${css.buttonSmall}`}
373
- disabled={tableBusy}
374
- onClick={() => { void generateTable() }}
375
- >
376
- 🔄 重新生成分镜表
377
- </button>
378
- <button
379
- type="button"
380
- className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
381
- onClick={() => { if (prompts === null) void generatePrompts(); onGoStep?.(3) }}
382
- >
383
- 🎬 下一步:生成视频提示词
384
- </button>
385
- </div>
386
- {table.shots.map(s => {
387
- const beat = skeleton?.beats.find(b => b.id === s.beatId)
388
- return (
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)' }}>
390
- <div className={css.row} style={{ flexWrap: 'wrap' }}>
391
- <b>镜头 {s.id}</b>
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
- )}
401
- </div>
402
- {s.jimengCamera !== undefined && s.jimengCamera !== '' && (
403
- <span className={css.meta}>🎥 即梦运镜:{s.jimengCamera}</span>
404
- )}
405
- <span>🎞️ {s.visual}</span>
406
- <div className={css.row} style={{ flexWrap: 'wrap' }}>
407
- <span className={css.meta}>💬 {s.line !== '' ? s.line : '(无台词)'}</span>
408
- <span className={css.meta}>🔊 {s.sound !== '' ? s.sound : '(无音效)'}</span>
409
- <span className={css.meta}>💡 {lightZh(s.light) !== '' ? lightZh(s.light) : '(无光效)'}</span>
410
- </div>
411
- <span className={css.meta}>承接:{s.prevState} → {s.nextState}</span>
412
- </div>
413
- )
414
- })}
415
- </div>
416
- )}
417
-
418
- {step === 3 && prompts !== null && (
419
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', marginTop: 'var(--nf-space-10)' }}>
420
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
421
- <b>🎬 视频提示词({prompts.length} 个镜头)</b>
422
- <button
423
- type="button"
424
- className={`${css.button} ${css.buttonSmall}`}
425
- disabled={promptsBusy}
426
- onClick={() => { void generatePrompts() }}
427
- >
428
- 🔄 重新生成视频提示词
429
- </button>
430
- <button
431
- type="button"
432
- className={`${css.button} ${css.buttonSmall}`}
433
- onClick={() => {
434
- const text = prompts.map(x => `【镜头 ${x.shotId}】${x.mangaRoleIds !== undefined && x.mangaRoleIds.length > 0 ? '定妆:' + bindingNames(x.mangaRoleIds) + ' ' : ''}${x.text}`).join('\n\n')
435
- void navigator.clipboard?.writeText(`第${table?.chapterNo ?? ''}章视频提示词\n\n` + text)
436
- }}
437
- >
438
- 📋 复制全部(即梦可逐条粘贴)
439
- </button>
440
- </div>
441
- {prompts.map(x => (
442
- <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)' }}>
443
- <div className={css.row} style={{ flexWrap: 'wrap' }}>
444
- <b>镜头 {x.shotId}</b>
445
- <span className={css.meta}>即梦/Seedance 提示词</span>
446
- {(() => { const shot = table?.shots.find(s => s.id === x.shotId); return shot !== undefined ? (
447
- <span className={css.meta}>{shot.duration}s · {shot.jimengCamera !== undefined && shot.jimengCamera !== '' ? shot.jimengCamera : cameraZh(shot.camera)}</span>
448
- ) : null })()}
449
- {x.mangaRoleIds !== undefined && x.mangaRoleIds.length > 0 && (
450
- <span className={css.badge + ' ' + css.badgeDone}>🎨 定妆 {bindingNames(x.mangaRoleIds)}</span>
451
- )}
452
- <div style={{ flex: 1 }} />
453
- <button
454
- type="button"
455
- className={`${css.button} ${css.buttonSmall}`}
456
- onClick={() => {
457
- void navigator.clipboard?.writeText(x.text)
458
- setCopied(x.shotId)
459
- setTimeout(() => { setCopied('') }, 1500)
460
- }}
461
- >
462
- {copied === x.shotId ? '✅ 已复制' : '📋 复制'}
463
- </button>
464
- </div>
465
- <span style={{ fontSize: 'var(--nf-fs-14)', lineHeight: 1.7 }}>{x.text}</span>
466
- </div>
467
- ))}
468
- </div>
469
- )}
470
- </div>
471
- )
472
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * 风格卡片(可复用):效果图 + 风格名 + 特点 + 关键词。
3
- * 风格库浏览页与漫剧方案风格选择器共用。
4
- */
5
- import { useState, type MouseEvent } from 'react'
6
- import type { ArtStyle } from '../../style-library.ts'
7
- import { STYLE_CATEGORIES } from '../../style-library.ts'
8
-
9
- export function StyleCard({
10
- style,
11
- selected,
12
- onClick,
13
- }: {
14
- style: ArtStyle
15
- /** 选中态(高亮边框)。 */
16
- selected?: boolean
17
- /** 点击卡片(选风格)。 */
18
- onClick?: () => void
19
- }) {
20
- const [copied, setCopied] = useState('')
21
- const cat = STYLE_CATEGORIES.find(c => c.id === style.category)
22
-
23
- const copyKeywords = async (e: MouseEvent): Promise<void> => {
24
- e.stopPropagation()
25
- try {
26
- await navigator.clipboard.writeText(style.keywords)
27
- setCopied(style.id)
28
- setTimeout(() => { setCopied('') }, 1500)
29
- } catch { /* 剪贴板不可用时静默 */ }
30
- }
31
-
32
- return (
33
- <div
34
- onClick={onClick}
35
- style={{
36
- display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)',
37
- border: `1px solid ${selected === true ? 'var(--nf-accent)' : 'var(--nf-border)'}`,
38
- borderRadius: 'var(--nf-radius-14)', padding: 'var(--nf-space-10)', background: 'var(--nf-bg-raise)',
39
- cursor: onClick !== undefined ? 'pointer' : 'default',
40
- boxShadow: selected === true ? '0 0 0 2px var(--nf-accent-soft)' : 'none',
41
- }}
42
- >
43
- {/* 效果图(竖版 3:4,服务端缩略图 webp;无图回退占位) */}
44
- <div
45
- style={{
46
- aspectRatio: '3 / 4', borderRadius: 'var(--nf-radius-10)',
47
- position: 'relative', overflow: 'hidden',
48
- display: 'flex', alignItems: 'center', justifyContent: 'center',
49
- background: 'linear-gradient(145deg, var(--nf-bg-inset), color-mix(in srgb, var(--nf-accent) 12%, var(--nf-bg-inset)))',
50
- border: '1px solid var(--nf-border)',
51
- color: 'var(--nf-text-3)', fontSize: 'var(--nf-fs-12)', textAlign: 'center',
52
- }}
53
- >
54
- <img
55
- src={`/api/dsh-novel-forge/styles/image?id=${style.id}`}
56
- alt={style.name}
57
- loading="lazy"
58
- style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
59
- onError={e => { e.currentTarget.style.display = 'none' }}
60
- />
61
- 🖼️ 效果图待生成
62
- {selected === true && (
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
- )}
65
- </div>
66
-
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
- </div>
72
-
73
- <span style={{ fontSize: 'var(--nf-fs-12)', lineHeight: 1.5, color: 'var(--nf-text-2)' }}>{style.traits}</span>
74
-
75
- <div
76
- style={{
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
- display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
80
- cursor: 'copy',
81
- }}
82
- title="点击复制关键词"
83
- onClick={e => { void copyKeywords(e) }}
84
- >
85
- 关键词:{style.keywords}
86
- </div>
87
- {copied === style.id && <span style={{ color: 'var(--nf-ok, #46a758)', fontSize: 'var(--nf-fs-12)' }}>✅ 已复制</span>}
88
- </div>
89
- )
90
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * 风格库页面:4 个分类标签 + 风格卡片网格。
3
- * 卡片预留:风格效果图占位 + 特点说明 + 关键词(一键复制)。
4
- * 后续:新建漫剧方案时从这里选风格(基底 + 滤镜)。
5
- */
6
- import { useState } from 'react'
7
- import { STYLE_CATEGORIES, STYLE_LIBRARY, stylesByCategory, type StyleCategory } from '../../style-library.ts'
8
- import { StyleCard } from './StyleCard.tsx'
9
- import css from './panel.module.css'
10
-
11
- export function StyleGallery() {
12
- const [active, setActive] = useState<StyleCategory>('3d')
13
- const [copied, setCopied] = useState('')
14
-
15
- const copyKeywords = async (id: string, keywords: string): Promise<void> => {
16
- try {
17
- await navigator.clipboard.writeText(keywords)
18
- setCopied(id)
19
- setTimeout(() => { setCopied('') }, 1500)
20
- } catch { /* 剪贴板不可用时静默 */ }
21
- }
22
-
23
- const list = stylesByCategory(active)
24
-
25
- return (
26
- <div className={css.card}>
27
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
28
- <span className={css.cardTitle}>🎨 风格库</span>
29
- <span className={css.meta}>共 {STYLE_LIBRARY.length} 个内置模板 · 新建漫剧方案时选择(影视类可作滤镜叠加)</span>
30
- </div>
31
-
32
- {/* 分类标签 */}
33
- <div className={css.row} style={{ gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
34
- {STYLE_CATEGORIES.map(cat => (
35
- <button
36
- key={cat.id}
37
- type="button"
38
- className={`${css.button} ${active === cat.id ? css.buttonPrimary : ''}`}
39
- style={{ flex: 1, minWidth: 140 }}
40
- onClick={() => { setActive(cat.id) }}
41
- >
42
- {cat.icon} {cat.label}({stylesByCategory(cat.id).length})
43
- </button>
44
- ))}
45
- </div>
46
- <div className={css.meta}>
47
- {STYLE_CATEGORIES.find(c => c.id === active)?.desc}
48
- </div>
49
-
50
- {/* 风格卡片网格 */}
51
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 'var(--nf-space-12)', marginTop: 'var(--nf-space-6)' }}>
52
- {list.map(style => (
53
- <StyleCard key={style.id} style={style} />
54
- ))}
55
- </div>
56
- </div>
57
- )
58
- }