@waterwx/dsh-novel-forge 1.2.0 → 1.2.1-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.
- package/README.md +237 -349
- package/lib/client.js +54447 -10560
- package/lib/index.js +12008 -5071
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +36 -0
- package/lib/types/assets.d.ts +18 -3
- package/lib/types/assistant.d.ts +29 -0
- package/lib/types/author-assets.d.ts +25 -0
- package/lib/types/bookshelf.d.ts +11 -1
- package/lib/types/client/api.d.ts +147 -9
- package/lib/types/client/docx.d.ts +2 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/llmLive.d.ts +33 -0
- package/lib/types/client/locales.d.ts +28 -11
- package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/client/panel/AuthorHome.d.ts +34 -0
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +5 -1
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/ImportModal.d.ts +7 -0
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/ModelManager.d.ts +14 -0
- package/lib/types/client/panel/NovelPanel.d.ts +6 -1
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
- package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
- package/lib/types/client/panel/SettingsView.d.ts +19 -0
- package/lib/types/client/panel/ShelfView.d.ts +12 -1
- package/lib/types/client/panel/SubPage.d.ts +27 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/views.d.ts +25 -7
- package/lib/types/client/run-status.d.ts +19 -0
- package/lib/types/client/text.d.ts +9 -0
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +146 -40
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/index.d.ts +18 -2
- package/lib/types/llm-live.d.ts +44 -0
- package/lib/types/market-radar-scan.d.ts +19 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/novel-context.d.ts +80 -0
- package/lib/types/prompt-utils.d.ts +35 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +1029 -123
- package/lib/types/review-policy.d.ts +97 -0
- package/lib/types/routes.d.ts +4 -0
- package/lib/types/run.d.ts +3 -1
- package/lib/types/stage-contract.d.ts +60 -0
- package/package.json +51 -27
- package/scripts/patch-schemastery.mjs +47 -0
- package/src/action-guard.ts +127 -0
- package/src/ai-scan.ts +179 -0
- package/src/assets.ts +1774 -520
- package/src/assistant.ts +1082 -770
- package/src/author-assets.ts +149 -0
- package/src/bookshelf.ts +187 -151
- package/src/client/api.ts +684 -410
- package/src/client/docx.ts +37 -69
- package/src/client/index.ts +3 -1
- package/src/client/llmLive.ts +101 -0
- package/src/client/locales.ts +418 -383
- package/src/client/mount.tsx +1 -1
- package/src/client/panel/AdaptModeView.tsx +535 -0
- package/src/client/panel/AiTag.tsx +7 -0
- package/src/client/panel/AssetsTab.tsx +448 -458
- package/src/client/panel/AssistantTab.tsx +355 -301
- package/src/client/panel/AuthorAssetsView.tsx +206 -0
- package/src/client/panel/AuthorHome.tsx +309 -0
- package/src/client/panel/BookAnalysisView.tsx +63 -0
- package/src/client/panel/CmdPalette.tsx +102 -0
- package/src/client/panel/CreateBookView.tsx +366 -348
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
- package/src/client/panel/IdeaInspirationView.tsx +76 -0
- package/src/client/panel/ImportModal.tsx +303 -0
- package/src/client/panel/KnowledgeBaseView.tsx +125 -0
- package/src/client/panel/LiveFeedLog.tsx +67 -0
- package/src/client/panel/MarketRadarView.tsx +377 -0
- package/src/client/panel/ModelManager.tsx +205 -0
- package/src/client/panel/NovelPanel.tsx +5403 -4597
- package/src/client/panel/PageHeader.tsx +37 -0
- package/src/client/panel/ProgressConsole.tsx +63 -0
- package/src/client/panel/ReaderView.tsx +306 -306
- package/src/client/panel/ReasoningSection.tsx +31 -0
- package/src/client/panel/RunPanel.tsx +223 -204
- package/src/client/panel/SettingsView.tsx +305 -0
- package/src/client/panel/ShelfView.tsx +266 -236
- package/src/client/panel/SubPage.tsx +42 -0
- package/src/client/panel/WorldTab.tsx +202 -202
- package/src/client/panel/panel.module.css +5764 -2941
- package/src/client/panel/toast.ts +51 -0
- package/src/client/panel/views.tsx +401 -314
- package/src/client/run-status.ts +143 -0
- package/src/client/sidebar-entry.ts +1 -1
- package/src/client/text.ts +28 -0
- package/src/docx-core.ts +53 -0
- package/src/docx.ts +44 -83
- package/src/engine.ts +4231 -2787
- package/src/global-assets.ts +59 -0
- package/src/index.ts +311 -252
- package/src/llm-live.ts +68 -0
- package/src/market-radar-scan.ts +41 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/migrate-output.ts +76 -0
- package/src/novel-context.ts +286 -0
- package/src/prompting.ts +26 -0
- package/src/protocol.ts +2072 -1182
- package/src/review-policy.ts +161 -0
- package/src/routes.ts +3405 -2151
- package/src/run.ts +342 -330
- package/src/stage-contract.ts +189 -0
- package/lib/client.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/src/client/panel/BookshelfBar.tsx +0 -170
|
@@ -1,236 +1,266 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 书架首页视图:书卡网格(封面/书名/简介/进度)+ 搜索筛选 + 开书入口。
|
|
3
|
-
* 进入小说工坊默认展示;点击书卡进入该书工作台,+ 进入开书向导页。
|
|
4
|
-
*/
|
|
5
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
-
import
|
|
7
|
-
import type {
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (diff <
|
|
17
|
-
if (diff <
|
|
18
|
-
if (diff <
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<span className={css.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<span className={
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
const
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
if (
|
|
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
|
-
type="
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<
|
|
217
|
-
key={
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 书架首页视图:书卡网格(封面/书名/简介/进度)+ 搜索筛选 + 开书入口。
|
|
3
|
+
* 进入小说工坊默认展示;点击书卡进入该书工作台,+ 进入开书向导页。
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
6
|
+
import { BookOpen, Download, PenLine, Plus, Search, Wand2 } from 'lucide-react'
|
|
7
|
+
import type { NovelApi } from '../api.ts'
|
|
8
|
+
import type { BookshelfSnapshot } from '../../protocol.ts'
|
|
9
|
+
import css from './panel.module.css'
|
|
10
|
+
|
|
11
|
+
/** 相对时间(人性化:刚刚 / N 分钟前 / N 小时前 / N 天前 / 日期)。 */
|
|
12
|
+
function relativeTime(iso: string): string {
|
|
13
|
+
const t = new Date(iso).getTime()
|
|
14
|
+
if (!Number.isFinite(t)) return '—'
|
|
15
|
+
const diff = Date.now() - t
|
|
16
|
+
if (diff < 60_000) return '刚刚'
|
|
17
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`
|
|
18
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`
|
|
19
|
+
if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天前`
|
|
20
|
+
return new Date(t).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 一本书的状态标签。 */
|
|
24
|
+
function bookStatus(book: BookshelfSnapshot['books'][number]): 'none' | 'done' | 'active' {
|
|
25
|
+
if (!book.hasProject) return 'none'
|
|
26
|
+
return book.total > 0 && book.done >= book.total ? 'done' : 'active'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 一张书卡(封面懒加载)。 */
|
|
30
|
+
function BookCard({
|
|
31
|
+
api,
|
|
32
|
+
book,
|
|
33
|
+
active,
|
|
34
|
+
onOpen,
|
|
35
|
+
onRead,
|
|
36
|
+
}: {
|
|
37
|
+
api: NovelApi
|
|
38
|
+
book: BookshelfSnapshot['books'][number]
|
|
39
|
+
active: boolean
|
|
40
|
+
onOpen: () => void
|
|
41
|
+
onRead: () => void
|
|
42
|
+
}) {
|
|
43
|
+
const [cover, setCover] = useState<string | null>(null)
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!book.hasCover) return
|
|
47
|
+
let cancelled = false
|
|
48
|
+
void api.coverGet(book.outputDir).then(result => {
|
|
49
|
+
if (!cancelled) setCover(result.dataUrl)
|
|
50
|
+
}).catch(() => { /* best-effort */ })
|
|
51
|
+
return () => { cancelled = true }
|
|
52
|
+
}, [api, book.hasCover, book.outputDir])
|
|
53
|
+
|
|
54
|
+
const ratio = book.total > 0 ? Math.min(book.done / book.total, 1) : 0
|
|
55
|
+
const status = bookStatus(book)
|
|
56
|
+
const statusLabel = status === 'none' ? '未开书' : status === 'done' ? '已完结' : '进行中'
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={`${css.bookCard} ${active ? css.bookCardActive : ''}`}
|
|
61
|
+
onClick={onOpen}
|
|
62
|
+
title={`打开《${book.bookName}》`}
|
|
63
|
+
>
|
|
64
|
+
<div className={css.bookCardCover}>
|
|
65
|
+
{cover !== null ? (
|
|
66
|
+
<img src={cover} alt={`《${book.bookName}》封面`} />
|
|
67
|
+
) : (
|
|
68
|
+
<div className={css.bookCardCoverFallback}>
|
|
69
|
+
<span className={css.bookCardCoverTitle}>{book.bookName.slice(0, 4)}</span>
|
|
70
|
+
<span className={css.meta}>暂无封面</span>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
<div className={css.bookCardBody}>
|
|
75
|
+
<div className={css.bookCardTitleRow}>
|
|
76
|
+
<span className={css.bookCardName}>{book.bookName}</span>
|
|
77
|
+
<span className={`${css.badge} ${status === 'none' ? css.badgePending : status === 'done' ? css.badgeDone : css.badgeWritten}`}>
|
|
78
|
+
{statusLabel}
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
<span className={`${css.meta} ${css.bookCardBlurb}`} title={book.blurb ?? ''}>
|
|
82
|
+
{book.blurb !== undefined && book.blurb !== '' ? book.blurb : '暂无简介'}
|
|
83
|
+
</span>
|
|
84
|
+
<div className={css.bookCardProgressBar}>
|
|
85
|
+
<div className={css.bookCardProgressFill} style={{ width: `${Math.round(ratio * 100)}%` }} />
|
|
86
|
+
</div>
|
|
87
|
+
<div className={css.bookCardMetaRow}>
|
|
88
|
+
<span className={css.meta}>{book.total > 0 ? `已完成 ${book.done} / ${book.total} 章` : '尚未规划章节'}</span>
|
|
89
|
+
{book.hasProject && <span className={css.meta} title="最近活动时间">更新于 {relativeTime(book.updatedAt)}</span>}
|
|
90
|
+
</div>
|
|
91
|
+
<div className={css.bookCardActions} onClick={e => { e.stopPropagation() }}>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
95
|
+
onClick={onOpen}
|
|
96
|
+
title="进入创作工作台(大纲/章节/审稿/设定)"
|
|
97
|
+
>
|
|
98
|
+
<PenLine size={13} style={{ verticalAlign: -2 }} /> 进入工作台
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className={`${css.button} ${css.buttonSmall}`}
|
|
103
|
+
disabled={!book.hasProject || book.done === 0}
|
|
104
|
+
onClick={onRead}
|
|
105
|
+
title={book.hasProject && book.done > 0 ? '沉浸式阅读已写章节' : '尚无已写章节,先去工作台创作'}
|
|
106
|
+
>
|
|
107
|
+
<BookOpen size={13} style={{ verticalAlign: -2 }} /> 阅读
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 书架首页。 */
|
|
116
|
+
export function ShelfView({
|
|
117
|
+
api,
|
|
118
|
+
shelf,
|
|
119
|
+
onOpenBook,
|
|
120
|
+
onReadBook,
|
|
121
|
+
onAddBook,
|
|
122
|
+
onImportBook,
|
|
123
|
+
onOpenAdapt,
|
|
124
|
+
toolbarExtra,
|
|
125
|
+
}: {
|
|
126
|
+
api: NovelApi
|
|
127
|
+
shelf: BookshelfSnapshot
|
|
128
|
+
/** 书架工具行右端插槽:组入口按钮由 AuthorHome 下放(创作 / 灵感策划 / 资产)。 */
|
|
129
|
+
toolbarExtra?: ReactNode
|
|
130
|
+
/** 点击书卡:激活该书并进入工作台。 */
|
|
131
|
+
onOpenBook: (id: string) => void
|
|
132
|
+
/** 点击「阅读」:激活该书并进入沉浸式阅读页。 */
|
|
133
|
+
onReadBook: (id: string) => void
|
|
134
|
+
/** 点击+:进入开书向导页。 */
|
|
135
|
+
onAddBook: () => void
|
|
136
|
+
/** 点击「导入」:打开导入弹窗(已有项目目录 / txt/md 全本)。 */
|
|
137
|
+
onImportBook: () => void
|
|
138
|
+
/** 点击「改编模式」:进入改编六步向导(入口收在书架页头,与导入并排)。 */
|
|
139
|
+
onOpenAdapt?: () => void
|
|
140
|
+
}) {
|
|
141
|
+
const [query, setQuery] = useState('')
|
|
142
|
+
/** 筛选:all=全部 / active=进行中 / done=已完结 / none=未开书。 */
|
|
143
|
+
const [filter, setFilter] = useState<'all' | 'active' | 'done' | 'none'>('all')
|
|
144
|
+
|
|
145
|
+
const stats = useMemo(() => {
|
|
146
|
+
let active = 0, done = 0, none = 0
|
|
147
|
+
for (const b of shelf.books) {
|
|
148
|
+
const s = bookStatus(b)
|
|
149
|
+
if (s === 'none') none++
|
|
150
|
+
else if (s === 'done') done++
|
|
151
|
+
else active++
|
|
152
|
+
}
|
|
153
|
+
return { active, done, none }
|
|
154
|
+
}, [shelf.books])
|
|
155
|
+
|
|
156
|
+
const visible = useMemo(() => {
|
|
157
|
+
const q = query.trim().toLowerCase()
|
|
158
|
+
return shelf.books.filter(b => {
|
|
159
|
+
if (filter !== 'all' && bookStatus(b) !== filter) return false
|
|
160
|
+
if (q === '') return true
|
|
161
|
+
return b.bookName.toLowerCase().includes(q) || (b.blurb ?? '').toLowerCase().includes(q)
|
|
162
|
+
})
|
|
163
|
+
}, [shelf.books, query, filter])
|
|
164
|
+
|
|
165
|
+
const activeName = shelf.books.find(b => b.id === shelf.activeBookId)?.bookName ?? ''
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className={css.subPage}>
|
|
169
|
+
<div className={css.subPageHead}>
|
|
170
|
+
<span className={css.subPageTitle}>书架</span>
|
|
171
|
+
<span className={css.subPageMeta}>
|
|
172
|
+
{shelf.books.length} 本书{activeName !== '' ? ` · 当前书《${activeName}》` : ''}
|
|
173
|
+
</span>
|
|
174
|
+
<div className={css.subPageActions}>
|
|
175
|
+
{onOpenAdapt !== undefined && (
|
|
176
|
+
<button type="button" className={css.button} onClick={onOpenAdapt} title="导入全文 → 六步改编成新书">
|
|
177
|
+
<Wand2 size={13} style={{ verticalAlign: -2 }} /> 改编模式
|
|
178
|
+
</button>
|
|
179
|
+
)}
|
|
180
|
+
<button type="button" className={css.button} onClick={onImportBook} title="导入已有项目目录或 txt / md 全本">
|
|
181
|
+
<Download size={13} style={{ verticalAlign: -2 }} /> 导入已有小说
|
|
182
|
+
</button>
|
|
183
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} onClick={onAddBook} title="书名 + 大纲,开书即建项目">
|
|
184
|
+
<Plus size={13} style={{ verticalAlign: -2 }} /> 开一本新书
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<div className={css.subPageBody}>
|
|
189
|
+
<div className={css.shelfToolbar}>
|
|
190
|
+
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
191
|
+
<Search size={15} style={{ position: 'absolute', left: 10, color: 'var(--nf-text-3)', pointerEvents: 'none' }} />
|
|
192
|
+
<input
|
|
193
|
+
className={`${css.input} ${css.shelfSearch}`}
|
|
194
|
+
type="search"
|
|
195
|
+
placeholder="搜索书名 / 简介…"
|
|
196
|
+
value={query}
|
|
197
|
+
onChange={e => { setQuery(e.target.value) }}
|
|
198
|
+
style={{ paddingLeft: 'var(--nf-space-30, 30px)' }}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
{toolbarExtra !== undefined && (
|
|
202
|
+
<div className={css.shelfToolbarExtra} role="tablist" aria-label="创作组入口">
|
|
203
|
+
{toolbarExtra}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{shelf.books.length > 0 && (
|
|
209
|
+
<div className={css.shelfStatGrid}>
|
|
210
|
+
{[
|
|
211
|
+
{ id: 'all' as const, label: '总书数', value: shelf.books.length },
|
|
212
|
+
{ id: 'active' as const, label: '进行中', value: stats.active },
|
|
213
|
+
{ id: 'done' as const, label: '已完结', value: stats.done },
|
|
214
|
+
{ id: 'none' as const, label: '未开书', value: stats.none },
|
|
215
|
+
].map(s => (
|
|
216
|
+
<div
|
|
217
|
+
key={s.id}
|
|
218
|
+
className={css.shelfStatCard}
|
|
219
|
+
data-active={filter === s.id ? '' : undefined}
|
|
220
|
+
title={s.id === 'all' ? '显示全部书籍' : `只看「${s.label}」的书`}
|
|
221
|
+
onClick={() => { setFilter(s.id) }}
|
|
222
|
+
>
|
|
223
|
+
<span>{s.label}</span>
|
|
224
|
+
<b className={`${css.numSerif} ${css.tabular}`}>{s.value}</b>
|
|
225
|
+
</div>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{shelf.books.length === 0 ? (
|
|
231
|
+
/* 空书架引导卡 */
|
|
232
|
+
<div className={css.shelfEmpty}>
|
|
233
|
+
<span className={css.shelfEmptyIcon}><BookOpen size={34} /></span>
|
|
234
|
+
<span className={css.shelfEmptyTitle}>你的创作从这里开始</span>
|
|
235
|
+
<span className={css.meta}>开一本书:粘贴大纲或导入 docx,书名自动识别,开书即建项目</span>
|
|
236
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} onClick={onAddBook}>
|
|
237
|
+
+ 开第一本书
|
|
238
|
+
</button>
|
|
239
|
+
<button type="button" className={`${css.button}`} onClick={onImportBook}>
|
|
240
|
+
导入已有小说
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
) : visible.length === 0 ? (
|
|
244
|
+
<div className={css.shelfEmpty} style={{ minHeight: 160 }}>
|
|
245
|
+
<span className={css.shelfEmptyIcon}><Search size={34} /></span>
|
|
246
|
+
<span className={css.shelfEmptyTitle}>没有符合条件的书</span>
|
|
247
|
+
<span className={css.meta}>换个关键词或筛选条件试试</span>
|
|
248
|
+
</div>
|
|
249
|
+
) : (
|
|
250
|
+
<div className={css.shelfGrid}>
|
|
251
|
+
{visible.map(book => (
|
|
252
|
+
<BookCard
|
|
253
|
+
key={book.id}
|
|
254
|
+
api={api}
|
|
255
|
+
book={book}
|
|
256
|
+
active={book.id === shelf.activeBookId}
|
|
257
|
+
onOpen={() => { onOpenBook(book.id) }}
|
|
258
|
+
onRead={() => { onReadBook(book.id) }}
|
|
259
|
+
/>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一子页骨架(1:1 对齐 demo-subpages.html 的 .subpage / .subpage-head / .subpage-body)。
|
|
3
|
+
*
|
|
4
|
+
* 所有作者级与书内子页都应套这层壳:
|
|
5
|
+
* - head:返回钮(可选)+ 大标题 + meta + 右侧操作区
|
|
6
|
+
* - body:页面内容(内部按 demo 用 .card / .box / .grid / .row 等块)
|
|
7
|
+
* 业务逻辑继续留在各自的 View 组件里,这里只负责「壳」。
|
|
8
|
+
*/
|
|
9
|
+
import type { ReactNode } from 'react'
|
|
10
|
+
import css from './panel.module.css'
|
|
11
|
+
|
|
12
|
+
export function SubPage(props: {
|
|
13
|
+
/** 页面大标题(衬线 display 档)。 */
|
|
14
|
+
title: string
|
|
15
|
+
/** 标题右侧的说明行(如「4 本书 · 当前书《X》」)。 */
|
|
16
|
+
meta?: ReactNode
|
|
17
|
+
/** 返回钮:作者级返回书架,书内返回总编台。 */
|
|
18
|
+
back?: { label: string; onClick: () => void }
|
|
19
|
+
/** 页头右侧操作区(主 CTA 放最右)。 */
|
|
20
|
+
actions?: ReactNode
|
|
21
|
+
/** 主题系统局部模式(透传到根 div 的 data-nf-mode,供设置页等预览明暗)。 */
|
|
22
|
+
dataNfMode?: 'light' | 'dark'
|
|
23
|
+
/** 主题系统局部密度(透传到根 div 的 data-nf-density)。 */
|
|
24
|
+
dataNfDensity?: 'comfort' | 'compact' | 'spacious'
|
|
25
|
+
children: ReactNode
|
|
26
|
+
}): JSX.Element {
|
|
27
|
+
return (
|
|
28
|
+
<div className={css.subPage} data-nf-mode={props.dataNfMode} data-nf-density={props.dataNfDensity}>
|
|
29
|
+
<div className={css.subPageHead}>
|
|
30
|
+
{props.back !== undefined && (
|
|
31
|
+
<button type="button" className={css.subPageBack} onClick={props.back.onClick}>
|
|
32
|
+
← {props.back.label}
|
|
33
|
+
</button>
|
|
34
|
+
)}
|
|
35
|
+
<span className={css.subPageTitle}>{props.title}</span>
|
|
36
|
+
{props.meta !== undefined && <span className={css.subPageMeta}>{props.meta}</span>}
|
|
37
|
+
{props.actions !== undefined && <div className={css.subPageActions}>{props.actions}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
<div className={css.subPageBody}>{props.children}</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|