@waterwx/dsh-novel-forge 1.1.8 → 1.2.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 (126) hide show
  1. package/README.md +231 -349
  2. package/lib/client.js +54833 -9926
  3. package/lib/index.js +12137 -4734
  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 +18 -3
  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 +153 -9
  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 +33 -0
  14. package/lib/types/client/locales.d.ts +34 -9
  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 +8 -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/ReasoningSection.d.ts +10 -0
  36. package/lib/types/client/panel/RunPanel.d.ts +5 -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/toast.d.ts +2 -0
  41. package/lib/types/client/panel/views.d.ts +25 -7
  42. package/lib/types/client/run-status.d.ts +19 -0
  43. package/lib/types/client/text.d.ts +9 -0
  44. package/lib/types/docx-core.d.ts +16 -0
  45. package/lib/types/docx.d.ts +2 -3
  46. package/lib/types/engine.d.ts +152 -40
  47. package/lib/types/global-assets.d.ts +6 -0
  48. package/lib/types/index.d.ts +20 -2
  49. package/lib/types/llm-live.d.ts +44 -0
  50. package/lib/types/market-radar-scan.d.ts +19 -0
  51. package/lib/types/market-radar-sources.d.ts +31 -0
  52. package/lib/types/migrate-output.d.ts +25 -0
  53. package/lib/types/novel-context.d.ts +80 -0
  54. package/lib/types/prompt-utils.d.ts +35 -0
  55. package/lib/types/prompting.d.ts +10 -0
  56. package/lib/types/protocol.d.ts +1106 -123
  57. package/lib/types/review-policy.d.ts +97 -0
  58. package/lib/types/routes.d.ts +4 -0
  59. package/lib/types/run.d.ts +34 -0
  60. package/lib/types/stage-contract.d.ts +60 -0
  61. package/package.json +52 -27
  62. package/scripts/patch-schemastery.mjs +47 -0
  63. package/src/action-guard.ts +127 -0
  64. package/src/ai-scan.ts +179 -0
  65. package/src/assets.ts +1774 -520
  66. package/src/assistant.ts +1082 -770
  67. package/src/author-assets.ts +149 -0
  68. package/src/bookshelf.ts +187 -151
  69. package/src/client/api.ts +684 -387
  70. package/src/client/docx.ts +37 -69
  71. package/src/client/index.ts +3 -1
  72. package/src/client/llmLive.ts +101 -0
  73. package/src/client/locales.ts +418 -367
  74. package/src/client/mount.tsx +1 -1
  75. package/src/client/panel/AdaptModeView.tsx +535 -0
  76. package/src/client/panel/AiTag.tsx +7 -0
  77. package/src/client/panel/AssetsTab.tsx +448 -439
  78. package/src/client/panel/AssistantTab.tsx +355 -301
  79. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  80. package/src/client/panel/AuthorHome.tsx +309 -0
  81. package/src/client/panel/BookAnalysisView.tsx +63 -0
  82. package/src/client/panel/CmdPalette.tsx +102 -0
  83. package/src/client/panel/CreateBookView.tsx +366 -348
  84. package/src/client/panel/DirectorView.tsx +200 -0
  85. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  86. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  87. package/src/client/panel/ImportModal.tsx +303 -0
  88. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  89. package/src/client/panel/LiveFeedLog.tsx +67 -0
  90. package/src/client/panel/MarketRadarView.tsx +377 -0
  91. package/src/client/panel/ModelManager.tsx +205 -0
  92. package/src/client/panel/NovelPanel.tsx +5403 -4294
  93. package/src/client/panel/PageHeader.tsx +37 -0
  94. package/src/client/panel/ProgressConsole.tsx +63 -0
  95. package/src/client/panel/ReaderView.tsx +306 -306
  96. package/src/client/panel/ReasoningSection.tsx +31 -0
  97. package/src/client/panel/RunPanel.tsx +223 -0
  98. package/src/client/panel/SettingsView.tsx +305 -0
  99. package/src/client/panel/ShelfView.tsx +266 -235
  100. package/src/client/panel/SubPage.tsx +42 -0
  101. package/src/client/panel/WorldTab.tsx +202 -202
  102. package/src/client/panel/panel.module.css +5764 -2817
  103. package/src/client/panel/toast.ts +51 -0
  104. package/src/client/panel/views.tsx +401 -314
  105. package/src/client/run-status.ts +143 -0
  106. package/src/client/sidebar-entry.ts +1 -1
  107. package/src/client/text.ts +28 -0
  108. package/src/docx-core.ts +53 -0
  109. package/src/docx.ts +44 -83
  110. package/src/engine.ts +4231 -2746
  111. package/src/global-assets.ts +59 -0
  112. package/src/index.ts +311 -246
  113. package/src/llm-live.ts +68 -0
  114. package/src/market-radar-scan.ts +41 -0
  115. package/src/market-radar-sources.ts +173 -0
  116. package/src/migrate-output.ts +76 -0
  117. package/src/novel-context.ts +286 -0
  118. package/src/prompting.ts +26 -0
  119. package/src/protocol.ts +2072 -1104
  120. package/src/review-policy.ts +161 -0
  121. package/src/routes.ts +3405 -2050
  122. package/src/run.ts +342 -0
  123. package/src/stage-contract.ts +189 -0
  124. package/lib/client.js.map +0 -1
  125. package/lib/index.js.map +0 -1
  126. 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
+ }