@waterwx/dsh-novel-forge 1.6.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +37690 -2269
- package/lib/client.js.map +1 -1
- package/lib/index.js +767 -56
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +3 -0
- package/lib/types/client/locales.d.ts +0 -1
- package/lib/types/client/panel/FlowGuide.d.ts +21 -0
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +18 -0
- package/lib/types/client/panel/MangaWorkspace.d.ts +6 -4
- package/lib/types/client/panel/RoleVisualPanel.d.ts +5 -3
- package/lib/types/client/panel/SceneLibrary.d.ts +1 -1
- package/lib/types/client/panel/StoryboardTab.d.ts +6 -2
- package/lib/types/engine.d.ts +23 -9
- package/lib/types/index.d.ts +6 -4
- package/lib/types/protocol.d.ts +171 -3
- package/package.json +100 -98
- package/src/client/api.ts +9 -0
- package/src/client/locales.ts +0 -1
- package/src/client/panel/AssetsTab.tsx +22 -22
- package/src/client/panel/AssistantTab.tsx +3 -3
- package/src/client/panel/BookshelfBar.tsx +1 -1
- package/src/client/panel/CreateBookView.tsx +10 -10
- package/src/client/panel/FlowGuide.tsx +83 -0
- package/src/client/panel/MangaRoleLibrary.tsx +810 -0
- package/src/client/panel/MangaWorkspace.tsx +359 -293
- package/src/client/panel/NovelPanel.tsx +465 -307
- package/src/client/panel/ReaderView.tsx +4 -4
- package/src/client/panel/RunPanel.tsx +9 -9
- package/src/client/panel/SceneLibrary.tsx +69 -68
- package/src/client/panel/ShelfView.tsx +28 -35
- package/src/client/panel/StoryboardTab.tsx +106 -42
- package/src/client/panel/StyleCard.tsx +13 -13
- package/src/client/panel/StyleGallery.tsx +2 -2
- package/src/client/panel/WorldTab.tsx +5 -5
- package/src/client/panel/panel.module.css +1189 -454
- package/src/client/panel/views.tsx +24 -24
- package/src/engine.ts +530 -59
- package/src/index.ts +37 -6
- package/src/protocol.ts +175 -3
- package/src/routes.ts +229 -1
- package/src/client/panel/RoleVisualPanel.tsx +0 -468
|
@@ -178,7 +178,7 @@ export function ReaderView({
|
|
|
178
178
|
<button
|
|
179
179
|
type="button"
|
|
180
180
|
className={`${css.button} ${css.buttonSmall} ${navOpen ? css.buttonPrimary : ''}`}
|
|
181
|
-
style={{ marginLeft: 4 }}
|
|
181
|
+
style={{ marginLeft: 'var(--nf-space-4)' }}
|
|
182
182
|
onClick={() => { setNavOpen(v => { const next = !v; writePref(`${prefKey}.nav`, next); return next }) }}
|
|
183
183
|
title={navOpen ? '收起目录' : '展开目录'}
|
|
184
184
|
>
|
|
@@ -200,7 +200,7 @@ export function ReaderView({
|
|
|
200
200
|
</button>
|
|
201
201
|
))}
|
|
202
202
|
</div>
|
|
203
|
-
<div className={css.readerSeg} style={{ marginLeft: 8 }}>
|
|
203
|
+
<div className={css.readerSeg} style={{ marginLeft: 'var(--nf-space-8)' }}>
|
|
204
204
|
{THEMES.map(t => (
|
|
205
205
|
<button
|
|
206
206
|
key={t.id}
|
|
@@ -214,7 +214,7 @@ export function ReaderView({
|
|
|
214
214
|
</button>
|
|
215
215
|
))}
|
|
216
216
|
</div>
|
|
217
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ marginLeft: 10 }} onClick={onOpenWorkspace} title="回到创作工作台">
|
|
217
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ marginLeft: 'var(--nf-space-10)' }} onClick={onOpenWorkspace} title="回到创作工作台">
|
|
218
218
|
✏️ 去工作台
|
|
219
219
|
</button>
|
|
220
220
|
</div>
|
|
@@ -224,7 +224,7 @@ export function ReaderView({
|
|
|
224
224
|
{navOpen && (
|
|
225
225
|
<aside className={css.readerSidebar} style={{ borderColor: themeMeta.dim, background: theme === 'night' ? 'rgba(0,0,0,0.25)' : 'rgba(255,255,255,0.35)' }} ref={sidebarRef}>
|
|
226
226
|
{groups.length === 0 ? (
|
|
227
|
-
<div className={css.meta} style={{ color: themeMeta.dim, padding: 12 }}>还没有已写章节,先去工作台创作吧</div>
|
|
227
|
+
<div className={css.meta} style={{ color: themeMeta.dim, padding: 'var(--nf-space-12)' }}>还没有已写章节,先去工作台创作吧</div>
|
|
228
228
|
) : groups.map(g => {
|
|
229
229
|
const collapsed = collapsedVols.includes(g.no)
|
|
230
230
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 生产单面板:区间批量生产(计划补足 → 逐章生成 → 被拒分级处理 → 断点续跑)。
|
|
3
|
-
*
|
|
3
|
+
* 与「AI进度」悬浮窗不同:这是标准流水线设备,一键下单、实时进度、日志可查。
|
|
4
4
|
*/
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
6
6
|
import type { NovelApi } from '../api.ts'
|
|
@@ -89,7 +89,7 @@ export function RunPanel({ api, totalChapters }: { api: NovelApi; totalChapters:
|
|
|
89
89
|
: 0
|
|
90
90
|
|
|
91
91
|
return (
|
|
92
|
-
<div className={css.card} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
92
|
+
<div className={css.card} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)' }}>
|
|
93
93
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
94
94
|
<span className={css.cardTitle}>🏭 生产单(标准流水线:计划 → 生成 → 审稿 → 分级处理)</span>
|
|
95
95
|
<span className={`${css.badge} ${run?.status === 'running' ? css.badgeWritten : run?.status === 'done' ? css.badgeDone : css.badgePending}`}>{statusLabel}</span>
|
|
@@ -100,8 +100,8 @@ export function RunPanel({ api, totalChapters }: { api: NovelApi; totalChapters:
|
|
|
100
100
|
</div>
|
|
101
101
|
|
|
102
102
|
{/* 下单区 */}
|
|
103
|
-
<div className={css.row} style={{ flexWrap: 'wrap', gap: 8, alignItems: 'flex-end' }}>
|
|
104
|
-
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
103
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-8)', alignItems: 'flex-end' }}>
|
|
104
|
+
<div style={{ display: 'flex', gap: 'var(--nf-space-4)', alignItems: 'center' }}>
|
|
105
105
|
<button
|
|
106
106
|
type="button"
|
|
107
107
|
className={`${css.button} ${css.buttonSmall} ${mode === 'count' ? css.buttonPrimary : ''}`}
|
|
@@ -162,12 +162,12 @@ export function RunPanel({ api, totalChapters }: { api: NovelApi; totalChapters:
|
|
|
162
162
|
)}
|
|
163
163
|
</div>
|
|
164
164
|
|
|
165
|
-
{err !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 12 }}>{err}</div>}
|
|
165
|
+
{err !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{err}</div>}
|
|
166
166
|
|
|
167
167
|
{/* 进度区 */}
|
|
168
168
|
{run !== null && (
|
|
169
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
170
|
-
<div className={css.row} style={{ flexWrap: 'wrap', gap: 8 }}>
|
|
169
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
170
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-8)' }}>
|
|
171
171
|
<span className={css.meta}>范围:第 {run.startNo} - {run.endNo} 章 · 当前:第 {run.currentNo} 章</span>
|
|
172
172
|
<span className={css.meta}>新生成 {run.stats.generated} · 修订通过 {run.stats.revised} · 豁免 {run.stats.exempted} · 重生成 {run.stats.regenerated} · 失败 {run.stats.error}</span>
|
|
173
173
|
</div>
|
|
@@ -188,12 +188,12 @@ export function RunPanel({ api, totalChapters }: { api: NovelApi; totalChapters:
|
|
|
188
188
|
<div className={css.meta} style={{ fontWeight: 600 }}>运行日志({run?.log.length ?? 0})</div>
|
|
189
189
|
<div
|
|
190
190
|
ref={logRef}
|
|
191
|
-
style={{ flex: 1, minHeight: 120, overflowY: 'auto', border: '1px solid var(--nf-border)', borderRadius: 10, background: 'var(--nf-bg-inset)', padding: 8, display: 'flex', flexDirection: 'column', gap: 2, fontSize: 12 }}
|
|
191
|
+
style={{ flex: 1, minHeight: 120, overflowY: 'auto', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', background: 'var(--nf-bg-inset)', padding: 'var(--nf-space-8)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-2)', fontSize: 'var(--nf-fs-12)' }}
|
|
192
192
|
>
|
|
193
193
|
{run === null || run.log.length === 0 ? (
|
|
194
194
|
<span className={css.meta}>尚无日志——下单后这里会实时显示每章进度。</span>
|
|
195
195
|
) : run.log.map((l, i) => (
|
|
196
|
-
<div key={i} style={{ display: 'flex', gap: 6, opacity: 0.85 }}>
|
|
196
|
+
<div key={i} style={{ display: 'flex', gap: 'var(--nf-space-6)', opacity: 0.85 }}>
|
|
197
197
|
<span style={{ color: 'var(--nf-text-3)', flex: 'none' }}>{new Date(l.at).toLocaleTimeString('zh-CN', { hour12: false })}</span>
|
|
198
198
|
<span>{l.text}</span>
|
|
199
199
|
</div>
|
|
@@ -26,7 +26,7 @@ export function SceneLibrary({
|
|
|
26
26
|
styleId?: string
|
|
27
27
|
/** 可选滤镜风格 id。 */
|
|
28
28
|
filterId?: string
|
|
29
|
-
/**
|
|
29
|
+
/** 上报到「AI进度」控制台。 */
|
|
30
30
|
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
31
31
|
}) {
|
|
32
32
|
const [busy, setBusy] = useState(false)
|
|
@@ -188,11 +188,12 @@ export function SceneLibrary({
|
|
|
188
188
|
return (
|
|
189
189
|
<div ref={rootRef} className={css.card} style={{ flex: 1, minHeight: 0 }}>
|
|
190
190
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
191
|
-
<span className={css.cardTitle} style={{ fontSize:
|
|
192
|
-
<span className={css.meta}
|
|
191
|
+
<span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>🏞️ 场景库</span>
|
|
192
|
+
<span className={css.meta}>场景视觉锚点:提炼自正文,供漫剧分镜/生图锁定「在哪、什么氛围」</span>
|
|
193
|
+
<span className={css.meta} style={{ display: 'block', marginTop: 'var(--nf-space-2)' }}>流程位置:第⑧步(选做)· 前置:已写章节 · 分镜表会自动标注使用场景</span>
|
|
193
194
|
</div>
|
|
194
195
|
|
|
195
|
-
<div className={css.row} style={{ flexWrap: 'wrap', gap: 6, margin: '8px 0' }}>
|
|
196
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)', margin: '8px 0' }}>
|
|
196
197
|
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void extract() }}>
|
|
197
198
|
{busy ? '⏳ 提炼中…' : '✨ 按当前方案提炼场景'}
|
|
198
199
|
</button>
|
|
@@ -204,31 +205,31 @@ export function SceneLibrary({
|
|
|
204
205
|
|
|
205
206
|
{/* 视觉规则:只在主页面显示一次(浮窗内不再重复)。 */}
|
|
206
207
|
{(project.visualRules ?? []).length > 0 && (
|
|
207
|
-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center', margin: '4px 0 8px' }}>
|
|
208
|
-
<span className={css.meta} style={{ fontSize:
|
|
209
|
-
{(project.visualRules ?? []).map((r, i) => <span key={i} style={{ border: '1px solid var(--nf-warn, #b8860b)', borderRadius: 999, padding: '0
|
|
208
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-4)', alignItems: 'center', margin: '4px 0 8px' }}>
|
|
209
|
+
<span className={css.meta} style={{ fontSize: 'var(--nf-fs-12)' }}>⚠️ 本书视觉规则(已内嵌到所有提示词):</span>
|
|
210
|
+
{(project.visualRules ?? []).map((r, i) => <span key={i} style={{ border: '1px solid var(--nf-warn, #b8860b)', borderRadius: 'var(--nf-radius-999)', padding: '0 var(--nf-space-8)', fontSize: 'var(--nf-fs-12)', color: 'var(--nf-warn, #b8860b)' }}>{r}</span>)}
|
|
210
211
|
</div>
|
|
211
212
|
)}
|
|
212
213
|
|
|
213
214
|
{error !== '' && <div className={css.importError}>{error}</div>}
|
|
214
|
-
{notice !== '' && <div className={css.importResult} style={{ padding: '
|
|
215
|
+
{notice !== '' && <div className={css.importResult} style={{ padding: 'var(--nf-space-6) var(--nf-space-10)' }}>{notice}</div>}
|
|
215
216
|
|
|
216
217
|
{/* 编辑场景 */}
|
|
217
218
|
{draft !== null && (
|
|
218
|
-
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 10, padding: 10, marginBottom: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
219
|
-
<b style={{ fontSize:
|
|
220
|
-
<input className={css.input} style={{ fontSize: 12, padding: '
|
|
221
|
-
<input className={css.input} style={{ fontSize: 12, padding: '
|
|
222
|
-
<div className={css.row} style={{ gap: 6 }}>
|
|
223
|
-
<input className={css.input} style={{ fontSize: 12, padding: '
|
|
224
|
-
<input className={css.input} style={{ fontSize: 12, padding: '
|
|
219
|
+
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-10)', marginBottom: 'var(--nf-space-10)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
220
|
+
<b style={{ fontSize: 'var(--nf-fs-14)' }}>编辑场景</b>
|
|
221
|
+
<input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="场景名" value={draft.name} onChange={e => { setDraft({ ...draft, name: e.target.value }) }} />
|
|
222
|
+
<input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="一句话定位" value={draft.summary} onChange={e => { setDraft({ ...draft, summary: e.target.value }) }} />
|
|
223
|
+
<div className={css.row} style={{ gap: 'var(--nf-space-6)' }}>
|
|
224
|
+
<input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', flex: 1 }} placeholder="幕归属(第一幕后场…)" value={draft.act ?? ''} onChange={e => { setDraft({ ...draft, act: e.target.value }) }} />
|
|
225
|
+
<input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', flex: 1 }} placeholder="时间光态(雨夜/闭店后…)" value={draft.moment ?? ''} onChange={e => { setDraft({ ...draft, moment: e.target.value }) }} />
|
|
225
226
|
</div>
|
|
226
|
-
<textarea className={css.input} style={{ fontSize: 12, padding: '
|
|
227
|
-
<input className={css.input} style={{ fontSize: 12, padding: '
|
|
228
|
-
<textarea className={css.input} style={{ fontSize: 12, padding: '
|
|
229
|
-
<textarea className={css.input} style={{ fontSize: 12, padding: '
|
|
230
|
-
<textarea className={css.input} style={{ fontSize: 12, padding: '
|
|
231
|
-
<div className={css.row} style={{ gap: 6 }}>
|
|
227
|
+
<textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 48 }} placeholder="关键镜头(每行一条:人物动作+情绪+镜头)" value={(draft.beats ?? []).join('\n')} onChange={e => { setDraft({ ...draft, beats: e.target.value.split(/\n+/).map(x => x.trim()).filter(Boolean) }) }} />
|
|
228
|
+
<input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="人物在场状态(含标志物细节)" value={draft.characterState ?? ''} onChange={e => { setDraft({ ...draft, characterState: e.target.value }) }} />
|
|
229
|
+
<textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 60 }} placeholder="环境构成(每行一项)" value={(draft.elements ?? []).join('\n')} onChange={e => { setDraft({ ...draft, elements: e.target.value.split(/\n+/).map(x => x.trim()).filter(Boolean) }) }} />
|
|
230
|
+
<textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 60 }} placeholder="中文生图提示词" value={draft.zh} onChange={e => { setDraft({ ...draft, zh: e.target.value }) }} />
|
|
231
|
+
<textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 60 }} placeholder="英文生图提示词" value={draft.en} onChange={e => { setDraft({ ...draft, en: e.target.value }) }} />
|
|
232
|
+
<div className={css.row} style={{ gap: 'var(--nf-space-6)' }}>
|
|
232
233
|
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy || draft.name.trim() === ''} onClick={() => { void saveDraft() }}>保存</button>
|
|
233
234
|
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setDraft(null) }}>取消</button>
|
|
234
235
|
</div>
|
|
@@ -237,18 +238,18 @@ export function SceneLibrary({
|
|
|
237
238
|
|
|
238
239
|
{/* 候选场景 */}
|
|
239
240
|
{candidates !== null && candidates.length > 0 && (
|
|
240
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 10 }}>
|
|
241
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', marginBottom: 'var(--nf-space-10)' }}>
|
|
241
242
|
{candidates.map(s => (
|
|
242
|
-
<div key={s.name} style={{ border: '1px solid var(--nf-accent)', borderRadius: 10, padding: '
|
|
243
|
-
<div className={css.row} style={{ flexWrap: 'wrap', gap: 6, justifyContent: 'space-between' }}>
|
|
243
|
+
<div key={s.name} style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
244
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)', justifyContent: 'space-between' }}>
|
|
244
245
|
<span><b>{s.name}</b> <span className={css.meta}>· {(s.moods ?? []).join('、')}</span>{s.styleId !== undefined && <span className={css.badge}>🎨 {s.styleId}</span>}</span>
|
|
245
|
-
<span className={css.row} style={{ gap: 4 }}>
|
|
246
|
+
<span className={css.row} style={{ gap: 'var(--nf-space-4)' }}>
|
|
246
247
|
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void adopt(s) }}>采纳</button>
|
|
247
248
|
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setDraft(s); setCandidates(null) }}>修改后采纳</button>
|
|
248
249
|
</span>
|
|
249
250
|
</div>
|
|
250
|
-
<div className={css.meta} style={{ marginTop: 2 }}>{s.summary}</div>
|
|
251
|
-
<div className={css.meta} style={{ marginTop: 2, fontSize:
|
|
251
|
+
<div className={css.meta} style={{ marginTop: 'var(--nf-space-2)' }}>{s.summary}</div>
|
|
252
|
+
<div className={css.meta} style={{ marginTop: 'var(--nf-space-2)', fontSize: 'var(--nf-fs-12)' }}>要素:{(s.elements ?? []).join(';')}</div>
|
|
252
253
|
</div>
|
|
253
254
|
))}
|
|
254
255
|
</div>
|
|
@@ -261,20 +262,20 @@ export function SceneLibrary({
|
|
|
261
262
|
<span className={css.meta}>点「✨ 按当前方案提炼场景」自动建立,或手动新增</span>
|
|
262
263
|
</div>
|
|
263
264
|
) : (
|
|
264
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 10 }}>
|
|
265
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 'var(--nf-space-10)' }}>
|
|
265
266
|
{scenes.map(s => {
|
|
266
267
|
const cover = (s.gallery ?? [])[0]
|
|
267
268
|
return (
|
|
268
|
-
<div key={s.name} onClick={() => { openDetail(s.name) }} style={{ cursor: 'pointer', border: '1px solid var(--nf-border)', borderRadius: 12, overflow: 'hidden', background: 'var(--nf-bg-inset)', display: 'flex', flexDirection: 'column' }}>
|
|
269
|
+
<div key={s.name} onClick={() => { openDetail(s.name) }} style={{ cursor: 'pointer', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-12)', overflow: 'hidden', background: 'var(--nf-bg-inset)', display: 'flex', flexDirection: 'column' }}>
|
|
269
270
|
<div style={{ height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--nf-bg-inset)', borderBottom: '1px solid var(--nf-border)', position: 'relative' }}>
|
|
270
271
|
{cover !== undefined
|
|
271
272
|
? <img src={cover.dataUrl} alt={s.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
272
273
|
: <span className={css.meta}>暂无场景图</span>}
|
|
273
|
-
<span style={{ position: 'absolute', bottom: 4, right: 4, background: 'rgba(0,0,0,0.6)', color: '#fff', fontSize:
|
|
274
|
+
<span style={{ position: 'absolute', bottom: 4, right: 4, background: 'rgba(0,0,0,0.6)', color: '#fff', fontSize: 'var(--nf-fs-12)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-2) var(--nf-space-6)' }}>🖼 {(s.gallery ?? []).length}</span>
|
|
274
275
|
</div>
|
|
275
|
-
<div style={{ padding: 6, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
276
|
-
<b style={{ fontSize:
|
|
277
|
-
<div className={css.meta} style={{ fontSize:
|
|
276
|
+
<div style={{ padding: 'var(--nf-space-6)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-2)' }}>
|
|
277
|
+
<b style={{ fontSize: 'var(--nf-fs-14)' }}>{s.name}</b>{s.styleId !== undefined && <span className={css.badge} style={{ fontSize: 'var(--nf-fs-12)' }}>🎨 {s.styleId}</span>}
|
|
278
|
+
<div className={css.meta} style={{ fontSize: 'var(--nf-fs-12)' }}>{(s.moods ?? []).join('、') || s.summary.slice(0, 16)}</div>
|
|
278
279
|
</div>
|
|
279
280
|
</div>
|
|
280
281
|
)
|
|
@@ -284,60 +285,60 @@ export function SceneLibrary({
|
|
|
284
285
|
|
|
285
286
|
{/* 场景详情浮窗(portal 到 body,避免 .view 的 backdrop-filter 破坏 fixed 定位) */}
|
|
286
287
|
{detail !== undefined && createPortal(
|
|
287
|
-
<div style={{ ...portalVars, position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 2147483000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }} onClick={e => { if (e.target === e.currentTarget) setDetailName(null) }}>
|
|
288
|
-
<div style={{ position: 'relative', background: 'var(--nf-bg)', border: '1px solid var(--nf-border)', borderRadius: 16, padding: 16, width: 'min(860px, 100%)', maxHeight: '88vh', overflow: 'auto', marginTop: 24 }}>
|
|
288
|
+
<div style={{ ...portalVars, position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 2147483000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--nf-space-24)' }} onClick={e => { if (e.target === e.currentTarget) setDetailName(null) }}>
|
|
289
|
+
<div style={{ position: 'relative', background: 'var(--nf-bg)', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-16)', padding: 'var(--nf-space-16)', width: 'min(860px, 100%)', maxHeight: '88vh', overflow: 'auto', marginTop: 'var(--nf-space-24)' }}>
|
|
289
290
|
<button type="button" className={css.iconButton} style={{ position: 'absolute', top: 10, right: 10 }} title="关闭" aria-label="关闭" onClick={() => { setDetailName(null) }}>✕</button>
|
|
290
|
-
<div style={{ marginBottom: 12 }}>
|
|
291
|
-
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
|
|
292
|
-
<b style={{ fontSize: 20 }}>{detail.name}</b>
|
|
291
|
+
<div style={{ marginBottom: 'var(--nf-space-12)' }}>
|
|
292
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
293
|
+
<b style={{ fontSize: 'var(--nf-fs-20)' }}>{detail.name}</b>
|
|
293
294
|
{detail.styleId !== undefined && <span className={css.badge}>🎨 {detail.styleId}</span>}
|
|
294
295
|
{(detail.act !== undefined && detail.act !== '') && <span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{detail.act}</span>}
|
|
295
296
|
{(detail.moment !== undefined && detail.moment !== '') && <span className={css.meta}>{detail.moment}</span>}
|
|
296
297
|
</div>
|
|
297
|
-
<div className={css.meta} style={{ marginTop: 4 }}>{detail.summary}</div>
|
|
298
|
+
<div className={css.meta} style={{ marginTop: 'var(--nf-space-4)' }}>{detail.summary}</div>
|
|
298
299
|
</div>
|
|
299
300
|
|
|
300
301
|
{/* 图集 */}
|
|
301
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 8, marginBottom: 12 }}>
|
|
302
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 'var(--nf-space-8)', marginBottom: 'var(--nf-space-12)' }}>
|
|
302
303
|
{(detail.gallery ?? []).map(img => (
|
|
303
|
-
<div key={img.label} style={{ border: '1px solid var(--nf-border)', borderRadius: 10, overflow: 'hidden', position: 'relative' }}>
|
|
304
|
+
<div key={img.label} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', overflow: 'hidden', position: 'relative' }}>
|
|
304
305
|
<img src={img.dataUrl} alt={img.label} style={{ width: '100%', height: 100, objectFit: 'cover', display: 'block' }} />
|
|
305
|
-
<div style={{ background: 'rgba(0,0,0,0.55)', color: '#fff', fontSize:
|
|
306
|
-
<button type="button" className={css.iconButton} style={{ position: 'absolute', top: 2, right: 2, fontSize:
|
|
306
|
+
<div style={{ background: 'rgba(0,0,0,0.55)', color: '#fff', fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-2) var(--nf-space-6)', textAlign: 'center' }}>{img.label}</div>
|
|
307
|
+
<button type="button" className={css.iconButton} style={{ position: 'absolute', top: 2, right: 2, fontSize: 'var(--nf-fs-12)' }} title="删除" aria-label="删除" onClick={() => { void removeImage(detail.name, img.label) }}>×</button>
|
|
307
308
|
</div>
|
|
308
309
|
))}
|
|
309
310
|
{(detail.gallery ?? []).length === 0 && <span className={css.meta}>暂无场景图</span>}
|
|
310
311
|
</div>
|
|
311
312
|
|
|
312
313
|
{/* 场景信息(紧凑卡片) */}
|
|
313
|
-
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 10, padding: '
|
|
314
|
-
<b style={{ fontSize: 12 }}>场景信息</b>
|
|
315
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '6px 14px', marginTop: 4, lineHeight: 1.7 }}>
|
|
314
|
+
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', marginBottom: 'var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
315
|
+
<b style={{ fontSize: 'var(--nf-fs-12)' }}>场景信息</b>
|
|
316
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '6px 14px', marginTop: 'var(--nf-space-4)', lineHeight: 1.7 }}>
|
|
316
317
|
<div>
|
|
317
|
-
<b style={{ fontSize:
|
|
318
|
-
<div style={{ marginTop: 2 }}>{(detail.beats ?? []).map((b, i) => <div key={i} style={{ fontSize:
|
|
318
|
+
<b style={{ fontSize: 'var(--nf-fs-12)' }}>关键镜头</b>
|
|
319
|
+
<div style={{ marginTop: 'var(--nf-space-2)' }}>{(detail.beats ?? []).map((b, i) => <div key={i} style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-text-2)', marginTop: 'var(--nf-space-2)' }}>🎬 {b}</div>)}</div>
|
|
319
320
|
</div>
|
|
320
321
|
<div>
|
|
321
322
|
{(detail.characterState !== undefined && detail.characterState !== '') && (
|
|
322
|
-
<div style={{ marginBottom: 6 }}>
|
|
323
|
-
<b style={{ fontSize:
|
|
324
|
-
<div className={css.meta} style={{ marginTop: 2 }}>{detail.characterState}</div>
|
|
323
|
+
<div style={{ marginBottom: 'var(--nf-space-6)' }}>
|
|
324
|
+
<b style={{ fontSize: 'var(--nf-fs-12)' }}>人物状态</b>
|
|
325
|
+
<div className={css.meta} style={{ marginTop: 'var(--nf-space-2)' }}>{detail.characterState}</div>
|
|
325
326
|
</div>
|
|
326
327
|
)}
|
|
327
|
-
<b style={{ fontSize:
|
|
328
|
-
<div style={{ marginTop: 2 }}>{(detail.elements ?? []).map(e => <span key={e} style={{ border: '1px solid var(--nf-border)', borderRadius: 999, padding: '
|
|
329
|
-
<div style={{ marginTop: 6 }}><b style={{ fontSize:
|
|
330
|
-
<div style={{ marginTop: 4 }}><b style={{ fontSize:
|
|
331
|
-
<div style={{ marginTop: 4 }}><b style={{ fontSize:
|
|
328
|
+
<b style={{ fontSize: 'var(--nf-fs-12)' }}>环境构成</b>
|
|
329
|
+
<div style={{ marginTop: 'var(--nf-space-2)' }}>{(detail.elements ?? []).map(e => <span key={e} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-999)', padding: 'var(--nf-space-2) var(--nf-space-8)', fontSize: 'var(--nf-fs-12)', margin: '0 4px 4px 0', display: 'inline-block' }}>{e}</span>)}</div>
|
|
330
|
+
<div style={{ marginTop: 'var(--nf-space-6)' }}><b style={{ fontSize: 'var(--nf-fs-12)' }}>色调光影</b> <span className={css.meta}>{(detail.palette ?? []).join(';') || '—'}</span></div>
|
|
331
|
+
<div style={{ marginTop: 'var(--nf-space-4)' }}><b style={{ fontSize: 'var(--nf-fs-12)' }}>氛围</b> <span className={css.meta}>{(detail.moods ?? []).join('、') || '—'}</span></div>
|
|
332
|
+
<div style={{ marginTop: 'var(--nf-space-4)' }}><b style={{ fontSize: 'var(--nf-fs-12)' }}>依据</b> <span className={css.meta}>{detail.source || '—'}</span></div>
|
|
332
333
|
</div>
|
|
333
334
|
</div>
|
|
334
335
|
</div>
|
|
335
336
|
|
|
336
337
|
{/* 生图提示词:全局中英 + 复制全部 */}
|
|
337
|
-
<div style={{ marginBottom: 12 }}>
|
|
338
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', marginBottom: 6 }}>
|
|
339
|
-
<b style={{ fontSize:
|
|
340
|
-
<span className={css.row} style={{ gap: 6, flexWrap: 'wrap' }}>
|
|
338
|
+
<div style={{ marginBottom: 'var(--nf-space-12)' }}>
|
|
339
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', marginBottom: 'var(--nf-space-6)' }}>
|
|
340
|
+
<b style={{ fontSize: 'var(--nf-fs-14)' }}>🎨 生图提示词</b>
|
|
341
|
+
<span className={css.row} style={{ gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
|
|
341
342
|
<button type="button" className={`${css.button} ${css.buttonSmall} ${sceneLang === 'zh' ? css.buttonPrimary : ''}`} onClick={() => { setSceneLang('zh') }}>🇨🇳 中文</button>
|
|
342
343
|
<button type="button" className={`${css.button} ${css.buttonSmall} ${sceneLang === 'en' ? css.buttonPrimary : ''}`} onClick={() => { setSceneLang('en') }}>🇬🇧 English</button>
|
|
343
344
|
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => {
|
|
@@ -346,27 +347,27 @@ export function SceneLibrary({
|
|
|
346
347
|
}}>📋 复制全部</button>
|
|
347
348
|
</span>
|
|
348
349
|
</div>
|
|
349
|
-
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 8, padding: 8 }}>
|
|
350
|
+
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', padding: 'var(--nf-space-8)' }}>
|
|
350
351
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
351
|
-
<b style={{ fontSize:
|
|
352
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ padding: '
|
|
352
|
+
<b style={{ fontSize: 'var(--nf-fs-12)' }}>{sceneLang === 'zh' ? '🇨🇳 中文' : '🇬🇧 English'}</b>
|
|
353
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ padding: 'var(--nf-space-2) var(--nf-space-6)', fontSize: 'var(--nf-fs-12)' }} onClick={() => {
|
|
353
354
|
void navigator.clipboard?.writeText(sceneLang === 'zh' ? detail.zh : detail.en).then(() => { notify('已复制「' + detail.name + '」' + (sceneLang === 'zh' ? '中文' : '英文') + '提示词') }).catch(() => { /* ignore */ })
|
|
354
355
|
}}>复制</button>
|
|
355
356
|
</div>
|
|
356
|
-
<div className={css.meta} style={{ fontSize:
|
|
357
|
-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 8 }}>
|
|
358
|
-
{(detail.tags ?? []).map(t => <span key={t} style={{ border: '1px solid var(--nf-border)', borderRadius: 999, padding: '
|
|
357
|
+
<div className={css.meta} style={{ fontSize: 'var(--nf-fs-12)', lineHeight: 1.6, marginTop: 'var(--nf-space-4)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{sceneLang === 'zh' ? detail.zh : detail.en}</div>
|
|
358
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-4)', marginTop: 'var(--nf-space-8)' }}>
|
|
359
|
+
{(detail.tags ?? []).map(t => <span key={t} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-999)', padding: 'var(--nf-space-2) var(--nf-space-8)', fontSize: 'var(--nf-fs-12)' }}>{t}</span>)}
|
|
359
360
|
</div>
|
|
360
361
|
</div>
|
|
361
362
|
</div>
|
|
362
363
|
|
|
363
364
|
{/* 操作条 */}
|
|
364
|
-
<div style={{ borderTop: '1px solid var(--nf-border)', paddingTop: 10, display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
|
|
365
|
+
<div style={{ borderTop: '1px solid var(--nf-border)', paddingTop: 'var(--nf-space-10)', display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-6)', alignItems: 'center' }}>
|
|
365
366
|
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => {
|
|
366
367
|
const text = [detail.en, detail.tags.join(', ')].filter(Boolean).join('\n')
|
|
367
368
|
void navigator.clipboard?.writeText(text).then(() => { notify('已复制「' + detail.name + '」英文生图提示词') }).catch(() => { /* ignore */ })
|
|
368
369
|
}}>复制提示词</button>
|
|
369
|
-
<input className={css.input} style={{ width: 130, fontSize: 12, padding: '
|
|
370
|
+
<input className={css.input} style={{ width: 130, fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-6)' }} placeholder="图集标签" value={uploadLabel} onChange={e => { setUploadLabel(e.target.value) }} />
|
|
370
371
|
<input
|
|
371
372
|
ref={inputRef}
|
|
372
373
|
type="file"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 进入小说工坊默认展示;点击书卡进入该书工作台,+ 进入开书向导页。
|
|
4
4
|
*/
|
|
5
5
|
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
+
import { BookOpen, Download, Library, PenLine, Plus, Search } from 'lucide-react'
|
|
6
7
|
import type { NovelApi } from '../api.ts'
|
|
7
8
|
import type { BookshelfSnapshot } from '../../protocol.ts'
|
|
8
9
|
import css from './panel.module.css'
|
|
@@ -94,7 +95,7 @@ function BookCard({
|
|
|
94
95
|
onClick={onOpen}
|
|
95
96
|
title="进入创作工作台(大纲/章节/审稿/设定)"
|
|
96
97
|
>
|
|
97
|
-
|
|
98
|
+
<PenLine size={13} style={{ verticalAlign: -2 }} /> 进入工作台
|
|
98
99
|
</button>
|
|
99
100
|
<button
|
|
100
101
|
type="button"
|
|
@@ -103,7 +104,7 @@ function BookCard({
|
|
|
103
104
|
onClick={onRead}
|
|
104
105
|
title={book.hasProject && book.done > 0 ? '沉浸式阅读已写章节' : '尚无已写章节,先去工作台创作'}
|
|
105
106
|
>
|
|
106
|
-
|
|
107
|
+
<BookOpen size={13} style={{ verticalAlign: -2 }} /> 阅读
|
|
107
108
|
</button>
|
|
108
109
|
</div>
|
|
109
110
|
</div>
|
|
@@ -155,25 +156,11 @@ export function ShelfView({
|
|
|
155
156
|
})
|
|
156
157
|
}, [shelf.books, query, filter])
|
|
157
158
|
|
|
158
|
-
const filters: Array<{ id: 'all' | 'active' | 'done' | 'none'; label: string; count: number }> = [
|
|
159
|
-
{ id: 'all', label: '全部', count: shelf.books.length },
|
|
160
|
-
{ id: 'active', label: '进行中', count: stats.active },
|
|
161
|
-
{ id: 'done', label: '已完结', count: stats.done },
|
|
162
|
-
{ id: 'none', label: '未开书', count: stats.none },
|
|
163
|
-
]
|
|
164
|
-
|
|
165
159
|
return (
|
|
166
160
|
<div className={css.shelfView}>
|
|
167
|
-
<div className={css.
|
|
161
|
+
<div className={css.shelfHero}>
|
|
168
162
|
<div className={css.shelfTitleRow}>
|
|
169
|
-
<h2 className={css.panelTitle} style={{ margin: 0 }}
|
|
170
|
-
{shelf.books.length > 0 && (
|
|
171
|
-
<span className={css.meta}>
|
|
172
|
-
{shelf.books.length} 本书 · {stats.active} 进行中 · {stats.done} 已完结 · {stats.none} 未开书
|
|
173
|
-
</span>
|
|
174
|
-
)}
|
|
175
|
-
</div>
|
|
176
|
-
<div className={css.shelfToolbar}>
|
|
163
|
+
<h2 className={css.panelTitle} style={{ margin: 0 }}><Library size={20} style={{ verticalAlign: -3 }} /> AI小说工坊 · 书架</h2>
|
|
177
164
|
<input
|
|
178
165
|
className={`${css.input} ${css.shelfSearch}`}
|
|
179
166
|
type="search"
|
|
@@ -182,25 +169,31 @@ export function ShelfView({
|
|
|
182
169
|
onChange={e => { setQuery(e.target.value) }}
|
|
183
170
|
/>
|
|
184
171
|
</div>
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
172
|
+
{shelf.books.length > 0 && (
|
|
173
|
+
<div className={css.shelfStats}>
|
|
174
|
+
{[
|
|
175
|
+
{ id: 'all' as const, label: '总书数', value: shelf.books.length },
|
|
176
|
+
{ id: 'active' as const, label: '进行中', value: stats.active },
|
|
177
|
+
{ id: 'done' as const, label: '已完结', value: stats.done },
|
|
178
|
+
{ id: 'none' as const, label: '未开书', value: stats.none },
|
|
179
|
+
].map(s => (
|
|
180
|
+
<div
|
|
181
|
+
key={s.id}
|
|
182
|
+
className={`${css.shelfStat} ${filter === s.id ? css.shelfStatActive : ''}`}
|
|
183
|
+
title={s.id === 'all' ? '显示全部书籍' : `只看「${s.label}」的书(再点一次取消筛选)`}
|
|
184
|
+
onClick={() => { setFilter(prev => prev === s.id ? 'all' : s.id) }}
|
|
185
|
+
>
|
|
186
|
+
<b>{s.value}</b><span>{s.label}</span>
|
|
187
|
+
</div>
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
198
191
|
</div>
|
|
199
192
|
|
|
200
193
|
{shelf.books.length === 0 ? (
|
|
201
194
|
/* 空书架引导卡 */
|
|
202
195
|
<div className={css.shelfEmpty}>
|
|
203
|
-
<span className={css.shelfEmptyIcon}
|
|
196
|
+
<span className={css.shelfEmptyIcon}><BookOpen size={34} /></span>
|
|
204
197
|
<span className={css.shelfEmptyTitle}>你的创作从这里开始</span>
|
|
205
198
|
<span className={css.meta}>开一本书:粘贴大纲或导入 docx,书名自动识别,开书即建项目</span>
|
|
206
199
|
<button type="button" className={`${css.button} ${css.buttonPrimary}`} onClick={onAddBook}>
|
|
@@ -212,7 +205,7 @@ export function ShelfView({
|
|
|
212
205
|
</div>
|
|
213
206
|
) : visible.length === 0 ? (
|
|
214
207
|
<div className={css.shelfEmpty} style={{ minHeight: 160 }}>
|
|
215
|
-
<span className={css.shelfEmptyIcon}
|
|
208
|
+
<span className={css.shelfEmptyIcon}><Search size={34} /></span>
|
|
216
209
|
<span className={css.shelfEmptyTitle}>没有符合条件的书</span>
|
|
217
210
|
<span className={css.meta}>换个关键词或筛选条件试试</span>
|
|
218
211
|
</div>
|
|
@@ -231,14 +224,14 @@ export function ShelfView({
|
|
|
231
224
|
|
|
232
225
|
{/* 开书入口:进入独立向导页 */}
|
|
233
226
|
<div className={`${css.bookCard} ${css.bookAddCard}`} onClick={onAddBook}>
|
|
234
|
-
<div className={css.bookAddIcon}
|
|
227
|
+
<div className={css.bookAddIcon}><Plus size={28} /></div>
|
|
235
228
|
<span>开一本新书</span>
|
|
236
229
|
<span className={css.meta}>书名 + 大纲,开书即建项目</span>
|
|
237
230
|
</div>
|
|
238
231
|
|
|
239
232
|
{/* 导入入口:已有项目目录 / txt/md 全本 */}
|
|
240
233
|
<div className={`${css.bookCard} ${css.bookAddCard}`} onClick={onImportBook}>
|
|
241
|
-
<div className={css.bookAddIcon}
|
|
234
|
+
<div className={css.bookAddIcon}><Download size={28} /></div>
|
|
242
235
|
<span>导入已有小说</span>
|
|
243
236
|
<span className={css.meta}>项目目录或 txt/md 全本</span>
|
|
244
237
|
</div>
|