@waterwx/dsh-novel-forge 1.2.0 → 1.3.0-alpha

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 (141) hide show
  1. package/README.md +236 -349
  2. package/lib/client.js +56064 -10565
  3. package/lib/index.js +13922 -5218
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +36 -0
  6. package/lib/types/assets.d.ts +24 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +25 -0
  9. package/lib/types/bookshelf.d.ts +11 -1
  10. package/lib/types/client/api.d.ts +178 -10
  11. package/lib/types/client/docx.d.ts +2 -0
  12. package/lib/types/client/index.d.ts +1 -1
  13. package/lib/types/client/llmLive.d.ts +31 -0
  14. package/lib/types/client/locales.d.ts +28 -11
  15. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  16. package/lib/types/client/panel/AiTag.d.ts +1 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  18. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  19. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  20. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  21. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  22. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  23. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  24. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  25. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  26. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  27. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  28. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  29. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  30. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  31. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  32. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  33. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  34. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  35. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  36. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  37. package/lib/types/client/panel/SettingsView.d.ts +19 -0
  38. package/lib/types/client/panel/ShelfView.d.ts +12 -1
  39. package/lib/types/client/panel/SubPage.d.ts +27 -0
  40. package/lib/types/client/panel/TensionView.d.ts +14 -0
  41. package/lib/types/client/panel/TimelineView.d.ts +14 -0
  42. package/lib/types/client/panel/toast.d.ts +2 -0
  43. package/lib/types/client/panel/views.d.ts +25 -7
  44. package/lib/types/client/run-status.d.ts +19 -0
  45. package/lib/types/client/text.d.ts +9 -0
  46. package/lib/types/docx-core.d.ts +16 -0
  47. package/lib/types/docx.d.ts +2 -3
  48. package/lib/types/engine.d.ts +185 -41
  49. package/lib/types/global-assets.d.ts +6 -0
  50. package/lib/types/index.d.ts +22 -2
  51. package/lib/types/llm-live.d.ts +75 -0
  52. package/lib/types/llm-retry.d.ts +38 -0
  53. package/lib/types/market-radar-scan.d.ts +19 -0
  54. package/lib/types/market-radar-sources.d.ts +31 -0
  55. package/lib/types/migrate-output.d.ts +25 -0
  56. package/lib/types/novel-context.d.ts +80 -0
  57. package/lib/types/prompt-slots.d.ts +57 -0
  58. package/lib/types/prompt-utils.d.ts +35 -0
  59. package/lib/types/prompting.d.ts +10 -0
  60. package/lib/types/protocol.d.ts +1311 -125
  61. package/lib/types/review-policy.d.ts +97 -0
  62. package/lib/types/routes.d.ts +4 -0
  63. package/lib/types/run.d.ts +3 -1
  64. package/lib/types/snapshots.d.ts +46 -0
  65. package/lib/types/stage-contract.d.ts +60 -0
  66. package/lib/types/tension.d.ts +65 -0
  67. package/lib/types/timeline.d.ts +37 -0
  68. package/package.json +52 -27
  69. package/scripts/patch-schemastery.mjs +47 -0
  70. package/src/action-guard.ts +127 -0
  71. package/src/ai-scan.ts +179 -0
  72. package/src/assets.ts +1806 -520
  73. package/src/assistant.ts +1098 -770
  74. package/src/author-assets.ts +149 -0
  75. package/src/bookshelf.ts +187 -151
  76. package/src/client/api.ts +758 -410
  77. package/src/client/docx.ts +37 -69
  78. package/src/client/index.ts +3 -1
  79. package/src/client/llmLive.ts +111 -0
  80. package/src/client/locales.ts +418 -383
  81. package/src/client/mount.tsx +1 -1
  82. package/src/client/panel/AdaptModeView.tsx +535 -0
  83. package/src/client/panel/AiTag.tsx +7 -0
  84. package/src/client/panel/AssetsTab.tsx +448 -458
  85. package/src/client/panel/AssistantTab.tsx +355 -301
  86. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  87. package/src/client/panel/AuthorHome.tsx +309 -0
  88. package/src/client/panel/BookAnalysisView.tsx +63 -0
  89. package/src/client/panel/CmdPalette.tsx +102 -0
  90. package/src/client/panel/CreateBookView.tsx +366 -348
  91. package/src/client/panel/DirectorView.tsx +200 -0
  92. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  93. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  94. package/src/client/panel/ImportModal.tsx +303 -0
  95. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  96. package/src/client/panel/LiveFeedLog.tsx +204 -0
  97. package/src/client/panel/MarketRadarView.tsx +377 -0
  98. package/src/client/panel/ModelManager.tsx +205 -0
  99. package/src/client/panel/NovelPanel.tsx +5517 -4597
  100. package/src/client/panel/PageHeader.tsx +37 -0
  101. package/src/client/panel/ProgressConsole.tsx +63 -0
  102. package/src/client/panel/PromptSlotsView.tsx +97 -0
  103. package/src/client/panel/ReaderView.tsx +306 -306
  104. package/src/client/panel/ReasoningSection.tsx +31 -0
  105. package/src/client/panel/RunPanel.tsx +223 -204
  106. package/src/client/panel/SettingsView.tsx +340 -0
  107. package/src/client/panel/ShelfView.tsx +266 -236
  108. package/src/client/panel/SubPage.tsx +42 -0
  109. package/src/client/panel/TensionView.tsx +196 -0
  110. package/src/client/panel/TimelineView.tsx +192 -0
  111. package/src/client/panel/WorldTab.tsx +202 -202
  112. package/src/client/panel/panel.module.css +5764 -2941
  113. package/src/client/panel/toast.ts +51 -0
  114. package/src/client/panel/views.tsx +401 -314
  115. package/src/client/run-status.ts +143 -0
  116. package/src/client/sidebar-entry.ts +1 -1
  117. package/src/client/text.ts +28 -0
  118. package/src/docx-core.ts +53 -0
  119. package/src/docx.ts +44 -83
  120. package/src/engine.ts +4672 -2787
  121. package/src/global-assets.ts +59 -0
  122. package/src/index.ts +323 -252
  123. package/src/llm-live.ts +260 -0
  124. package/src/llm-retry.ts +77 -0
  125. package/src/market-radar-scan.ts +41 -0
  126. package/src/market-radar-sources.ts +173 -0
  127. package/src/migrate-output.ts +76 -0
  128. package/src/novel-context.ts +286 -0
  129. package/src/prompt-slots.ts +125 -0
  130. package/src/prompting.ts +26 -0
  131. package/src/protocol.ts +2383 -1182
  132. package/src/review-policy.ts +161 -0
  133. package/src/routes.ts +3724 -2151
  134. package/src/run.ts +346 -330
  135. package/src/snapshots.ts +174 -0
  136. package/src/stage-contract.ts +189 -0
  137. package/src/tension.ts +240 -0
  138. package/src/timeline.ts +159 -0
  139. package/lib/client.js.map +0 -1
  140. package/lib/index.js.map +0 -1
  141. package/src/client/panel/BookshelfBar.tsx +0 -170
@@ -1,202 +1,202 @@
1
- /**
2
- * 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 + AI 提炼。
3
- * 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
4
- */
5
- import { useEffect, useState } from 'react'
6
- import type { NovelApi } from '../api.ts'
7
- import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
8
- import css from './panel.module.css'
9
-
10
- const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
11
-
12
- /** 一个可编辑条目行。 */
13
- function EditableRow({
14
- name,
15
- detail,
16
- onName,
17
- onDetail,
18
- onRemove,
19
- namePlaceholder,
20
- detailPlaceholder,
21
- }: {
22
- name: string
23
- detail: string
24
- onName: (v: string) => void
25
- onDetail: (v: string) => void
26
- onRemove: () => void
27
- namePlaceholder: string
28
- detailPlaceholder: string
29
- }) {
30
- return (
31
- <div style={{ display: 'flex', gap: 6, alignItems: 'flex-start' }}>
32
- <input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
33
- <input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
34
- <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
35
- </div>
36
- )
37
- }
38
-
39
- /** 大世界页签。 */
40
- export function WorldTab({
41
- api,
42
- world,
43
- onChanged,
44
- }: {
45
- api: NovelApi
46
- /** 当前项目的大世界数据(可能为空)。 */
47
- world: WorldState | undefined
48
- /** 保存/提炼成功后由父组件刷新项目状态。 */
49
- onChanged: (world: WorldState) => void
50
- }) {
51
- const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
52
- const [busy, setBusy] = useState(false)
53
- const [error, setError] = useState('')
54
-
55
- // 外部数据变化时同步草稿(提炼/保存/切换书后)。
56
- useEffect(() => {
57
- setDraft(world ?? EMPTY)
58
- }, [world])
59
-
60
- const handleGenerate = async (): Promise<void> => {
61
- setBusy(true)
62
- setError('')
63
- try {
64
- const result = await api.world('generate')
65
- setDraft(result.world)
66
- onChanged(result.world)
67
- } catch (err) {
68
- setError((err as Error).message)
69
- } finally {
70
- setBusy(false)
71
- }
72
- }
73
-
74
- const handleSave = async (): Promise<void> => {
75
- setBusy(true)
76
- setError('')
77
- try {
78
- const result = await api.world('save', draft)
79
- onChanged(result.world)
80
- } catch (err) {
81
- setError((err as Error).message)
82
- } finally {
83
- setBusy(false)
84
- }
85
- }
86
-
87
- const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
88
- const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
89
- const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
90
-
91
- return (
92
- <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
93
- {error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
94
-
95
- <div className={css.card}>
96
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
97
- <span className={css.cardTitle}>大世界</span>
98
- <div className={css.row}>
99
- <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
100
- ✨ AI 提炼
101
- </button>
102
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
103
- 💾 保存
104
- </button>
105
- </div>
106
- </div>
107
- <span className={css.meta}>
108
- 境界体系按由低到高排序注入章节生成提示词,模型不得随意跳级或自创境界;区域与势力约束地理/势力设定。AI 提炼不满意可逐条编辑后保存。
109
- </span>
110
- </div>
111
-
112
- {/* 境界体系 */}
113
- <div className={css.card}>
114
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
115
- <span className={css.cardTitle}>境界体系({draft.realms.length})</span>
116
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
117
- + 新增境界
118
- </button>
119
- </div>
120
- {draft.realms.length === 0 ? (
121
- <span className={css.meta}>暂无境界体系 — 点击 ✨AI 提炼 或手动添加(由低到高)。</span>
122
- ) : (
123
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
124
- {draft.realms.map((realm, i) => (
125
- <EditableRow
126
- key={i}
127
- name={realm.name}
128
- detail={realm.description}
129
- onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
130
- onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
131
- onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
132
- namePlaceholder={`第 ${i + 1} 阶境界名`}
133
- detailPlaceholder="突破条件 / 寿命 / 标志…"
134
- />
135
- ))}
136
- </div>
137
- )}
138
- </div>
139
-
140
- {/* 地理区域 */}
141
- <div className={css.card}>
142
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
143
- <span className={css.cardTitle}>地理区域({draft.regions.length})</span>
144
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
145
- + 新增区域
146
- </button>
147
- </div>
148
- {draft.regions.length === 0 ? (
149
- <span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
150
- ) : (
151
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
152
- {draft.regions.map((region, i) => (
153
- <EditableRow
154
- key={i}
155
- name={region.name}
156
- detail={region.description}
157
- onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
158
- onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
159
- onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
160
- namePlaceholder="区域名(如大荒 / 青云山脉)"
161
- detailPlaceholder="描述…"
162
- />
163
- ))}
164
- </div>
165
- )}
166
- </div>
167
-
168
- {/* 势力分布 */}
169
- <div className={css.card}>
170
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
171
- <span className={css.cardTitle}>势力分布({draft.factions.length})</span>
172
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
173
- + 新增势力
174
- </button>
175
- </div>
176
- {draft.factions.length === 0 ? (
177
- <span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
178
- ) : (
179
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
180
- {draft.factions.map((faction, i) => (
181
- <EditableRow
182
- key={i}
183
- name={`${faction.name}(${faction.kind})`}
184
- detail={faction.description}
185
- onName={v => {
186
- const next = [...draft.factions]
187
- const match = /^(.*)((.*))$/.exec(v)
188
- next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
189
- setFactions(next)
190
- }}
191
- onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
192
- onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
193
- namePlaceholder="势力名(类型)如:青云宗(宗门)"
194
- detailPlaceholder="描述…"
195
- />
196
- ))}
197
- </div>
198
- )}
199
- </div>
200
- </div>
201
- )
202
- }
1
+ /**
2
+ * 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 + 提炼。
3
+ * 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
4
+ */
5
+ import { useEffect, useState } from 'react'
6
+ import type { NovelApi } from '../api.ts'
7
+ import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
8
+ import css from './panel.module.css'
9
+
10
+ const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
11
+
12
+ /** 一个可编辑条目行。 */
13
+ function EditableRow({
14
+ name,
15
+ detail,
16
+ onName,
17
+ onDetail,
18
+ onRemove,
19
+ namePlaceholder,
20
+ detailPlaceholder,
21
+ }: {
22
+ name: string
23
+ detail: string
24
+ onName: (v: string) => void
25
+ onDetail: (v: string) => void
26
+ onRemove: () => void
27
+ namePlaceholder: string
28
+ detailPlaceholder: string
29
+ }) {
30
+ return (
31
+ <div style={{ display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'flex-start' }}>
32
+ <input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
33
+ <input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
34
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ /** 大世界页签。 */
40
+ export function WorldTab({
41
+ api,
42
+ world,
43
+ onChanged,
44
+ }: {
45
+ api: NovelApi
46
+ /** 当前项目的大世界数据(可能为空)。 */
47
+ world: WorldState | undefined
48
+ /** 保存/提炼成功后由父组件刷新项目状态。 */
49
+ onChanged: (world: WorldState) => void
50
+ }) {
51
+ const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
52
+ const [busy, setBusy] = useState(false)
53
+ const [error, setError] = useState('')
54
+
55
+ // 外部数据变化时同步草稿(提炼/保存/切换书后)。
56
+ useEffect(() => {
57
+ setDraft(world ?? EMPTY)
58
+ }, [world])
59
+
60
+ const handleGenerate = async (): Promise<void> => {
61
+ setBusy(true)
62
+ setError('')
63
+ try {
64
+ const result = await api.world('generate')
65
+ setDraft(result.world)
66
+ onChanged(result.world)
67
+ } catch (err) {
68
+ setError((err as Error).message)
69
+ } finally {
70
+ setBusy(false)
71
+ }
72
+ }
73
+
74
+ const handleSave = async (): Promise<void> => {
75
+ setBusy(true)
76
+ setError('')
77
+ try {
78
+ const result = await api.world('save', draft)
79
+ onChanged(result.world)
80
+ } catch (err) {
81
+ setError((err as Error).message)
82
+ } finally {
83
+ setBusy(false)
84
+ }
85
+ }
86
+
87
+ const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
88
+ const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
89
+ const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
90
+
91
+ return (
92
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)' }}>
93
+ {error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
94
+
95
+ <div className={css.card}>
96
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
97
+ <span className={css.cardTitle}>大世界</span>
98
+ <div className={css.row}>
99
+ <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
100
+ 提炼
101
+ </button>
102
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
103
+ 保存
104
+ </button>
105
+ </div>
106
+ </div>
107
+ <span className={css.meta}>
108
+ 境界体系按由低到高排序注入章节生成提示词,模型不得随意跳级或自创境界;区域与势力约束地理/势力设定。提炼不满意可逐条编辑后保存。
109
+ </span>
110
+ </div>
111
+
112
+ {/* 境界体系 */}
113
+ <div className={css.card}>
114
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
115
+ <span className={css.cardTitle}>境界体系({draft.realms.length})</span>
116
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
117
+ + 新增境界
118
+ </button>
119
+ </div>
120
+ {draft.realms.length === 0 ? (
121
+ <span className={css.meta}>暂无境界体系 — 点击 提炼 或手动添加(由低到高)。</span>
122
+ ) : (
123
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
124
+ {draft.realms.map((realm, i) => (
125
+ <EditableRow
126
+ key={i}
127
+ name={realm.name}
128
+ detail={realm.description}
129
+ onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
130
+ onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
131
+ onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
132
+ namePlaceholder={`第 ${i + 1} 阶境界名`}
133
+ detailPlaceholder="突破条件 / 寿命 / 标志…"
134
+ />
135
+ ))}
136
+ </div>
137
+ )}
138
+ </div>
139
+
140
+ {/* 地理区域 */}
141
+ <div className={css.card}>
142
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
143
+ <span className={css.cardTitle}>地理区域({draft.regions.length})</span>
144
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
145
+ + 新增区域
146
+ </button>
147
+ </div>
148
+ {draft.regions.length === 0 ? (
149
+ <span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
150
+ ) : (
151
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
152
+ {draft.regions.map((region, i) => (
153
+ <EditableRow
154
+ key={i}
155
+ name={region.name}
156
+ detail={region.description}
157
+ onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
158
+ onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
159
+ onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
160
+ namePlaceholder="区域名(如大荒 / 青云山脉)"
161
+ detailPlaceholder="描述…"
162
+ />
163
+ ))}
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ {/* 势力分布 */}
169
+ <div className={css.card}>
170
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
171
+ <span className={css.cardTitle}>势力分布({draft.factions.length})</span>
172
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
173
+ + 新增势力
174
+ </button>
175
+ </div>
176
+ {draft.factions.length === 0 ? (
177
+ <span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
178
+ ) : (
179
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
180
+ {draft.factions.map((faction, i) => (
181
+ <EditableRow
182
+ key={i}
183
+ name={`${faction.name}(${faction.kind})`}
184
+ detail={faction.description}
185
+ onName={v => {
186
+ const next = [...draft.factions]
187
+ const match = /^(.*)((.*))$/.exec(v)
188
+ next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
189
+ setFactions(next)
190
+ }}
191
+ onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
192
+ onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
193
+ namePlaceholder="势力名(类型)如:青云宗(宗门)"
194
+ detailPlaceholder="描述…"
195
+ />
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ )
202
+ }