@waterwx/dsh-novel-forge 1.1.8 → 1.2.0
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 +2 -2
- package/lib/client.js +2262 -1242
- package/lib/client.js.map +1 -1
- package/lib/index.js +1017 -551
- package/lib/index.js.map +1 -1
- package/lib/types/bookshelf.d.ts +1 -1
- package/lib/types/client/api.d.ts +6 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/client/panel/RunPanel.d.ts +5 -0
- package/lib/types/engine.d.ts +7 -1
- package/lib/types/index.d.ts +3 -1
- package/lib/types/protocol.d.ts +77 -0
- package/lib/types/run.d.ts +32 -0
- package/package.json +2 -1
- package/src/bookshelf.ts +2 -2
- package/src/client/api.ts +23 -0
- package/src/client/locales.ts +16 -0
- package/src/client/panel/AssetsTab.tsx +19 -0
- package/src/client/panel/CreateBookView.tsx +3 -3
- package/src/client/panel/NovelPanel.tsx +528 -225
- package/src/client/panel/RunPanel.tsx +204 -0
- package/src/client/panel/ShelfView.tsx +13 -12
- package/src/client/panel/panel.module.css +192 -68
- package/src/engine.ts +42 -1
- package/src/index.ts +11 -5
- package/src/protocol.ts +78 -0
- package/src/routes.ts +105 -4
- package/src/run.ts +330 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生产单面板:区间批量生产(计划补足 → 逐章生成 → 被拒分级处理 → 断点续跑)。
|
|
3
|
+
* 与「工作进度」悬浮窗不同:这是标准流水线设备,一键下单、实时进度、日志可查。
|
|
4
|
+
*/
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
+
import type { NovelApi } from '../api.ts'
|
|
7
|
+
import type { RunState } from '../../protocol.ts'
|
|
8
|
+
import css from './panel.module.css'
|
|
9
|
+
|
|
10
|
+
export function RunPanel({ api, totalChapters }: { api: NovelApi; totalChapters: number }) {
|
|
11
|
+
const [run, setRun] = useState<RunState | null>(null)
|
|
12
|
+
const [startNo, setStartNo] = useState<number>(1)
|
|
13
|
+
const [endNo, setEndNo] = useState<number>(0)
|
|
14
|
+
const [count, setCount] = useState<number>(30)
|
|
15
|
+
const [mode, setMode] = useState<'count' | 'range'>('count')
|
|
16
|
+
const [busy, setBusy] = useState(false)
|
|
17
|
+
const [err, setErr] = useState('')
|
|
18
|
+
const logRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
|
|
20
|
+
// 默认起点:最后一章 + 1;count 模式终点 = 起点 + count - 1
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setStartNo(totalChapters + 1)
|
|
23
|
+
setEndNo(totalChapters + 30)
|
|
24
|
+
}, [totalChapters])
|
|
25
|
+
|
|
26
|
+
const poll = useCallback(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const s = await api.runStatus()
|
|
29
|
+
setRun(s)
|
|
30
|
+
} catch { /* 静默 */ }
|
|
31
|
+
}, [api])
|
|
32
|
+
|
|
33
|
+
// 运行期间每 5 秒轮询一次状态。
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
void poll()
|
|
36
|
+
const timer = window.setInterval(() => { void poll() }, 5000)
|
|
37
|
+
return () => window.clearInterval(timer)
|
|
38
|
+
}, [poll])
|
|
39
|
+
|
|
40
|
+
// 日志自动滚底。
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
logRef.current?.scrollTo({ top: logRef.current.scrollHeight })
|
|
43
|
+
}, [run?.log.length])
|
|
44
|
+
|
|
45
|
+
const effectiveEnd = mode === 'count' ? startNo + count - 1 : endNo
|
|
46
|
+
|
|
47
|
+
const handleStart = async (): Promise<void> => {
|
|
48
|
+
setBusy(true)
|
|
49
|
+
setErr('')
|
|
50
|
+
try {
|
|
51
|
+
const req = mode === 'count'
|
|
52
|
+
? { startNo, count }
|
|
53
|
+
: { startNo, endNo }
|
|
54
|
+
const s = await api.runStart(req)
|
|
55
|
+
setRun(s)
|
|
56
|
+
} catch (e) {
|
|
57
|
+
setErr((e as Error).message)
|
|
58
|
+
} finally {
|
|
59
|
+
setBusy(false)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handleControl = async (action: 'pause' | 'resume' | 'stop'): Promise<void> => {
|
|
64
|
+
setBusy(true)
|
|
65
|
+
setErr('')
|
|
66
|
+
try {
|
|
67
|
+
const s = await api.runControl(action)
|
|
68
|
+
if (s !== null) setRun(s)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
setErr((e as Error).message)
|
|
71
|
+
} finally {
|
|
72
|
+
setBusy(false)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const statusLabel = useMemo(() => {
|
|
77
|
+
if (run === null) return '未启动'
|
|
78
|
+
return {
|
|
79
|
+
running: '🏭 生产中',
|
|
80
|
+
paused: '⏸ 已暂停',
|
|
81
|
+
done: '✅ 已完成',
|
|
82
|
+
stopped: '⏹ 已停止',
|
|
83
|
+
error: '❌ 异常',
|
|
84
|
+
}[run.status] ?? run.status
|
|
85
|
+
}, [run])
|
|
86
|
+
|
|
87
|
+
const ratio = run !== null && run.endNo > run.startNo
|
|
88
|
+
? Math.min(Math.max((run.currentNo - run.startNo) / (run.endNo - run.startNo), 0), 1)
|
|
89
|
+
: 0
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={css.card} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
93
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
94
|
+
<span className={css.cardTitle}>🏭 生产单(标准流水线:计划 → 生成 → 审稿 → 分级处理)</span>
|
|
95
|
+
<span className={`${css.badge} ${run?.status === 'running' ? css.badgeWritten : run?.status === 'done' ? css.badgeDone : css.badgePending}`}>{statusLabel}</span>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className={css.meta}>
|
|
99
|
+
一键下生产单:自动补计划 → 逐章生成(完整质量门:生成→摘要+事实→审稿→作者复盘)→ 被拒章分级处理(无 high 豁免 / 有 high 按意见修订+验证 / 两轮不过转待人工)。中断后可从断点继续。
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* 下单区 */}
|
|
103
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 8, alignItems: 'flex-end' }}>
|
|
104
|
+
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
className={`${css.button} ${css.buttonSmall} ${mode === 'count' ? css.buttonPrimary : ''}`}
|
|
108
|
+
onClick={() => { setMode('count') }}
|
|
109
|
+
title="从当前末章 +1 起,新增 N 章"
|
|
110
|
+
>
|
|
111
|
+
新增 N 章
|
|
112
|
+
</button>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className={`${css.button} ${css.buttonSmall} ${mode === 'range' ? css.buttonPrimary : ''}`}
|
|
116
|
+
onClick={() => { setMode('range') }}
|
|
117
|
+
title="指定起止章号区间"
|
|
118
|
+
>
|
|
119
|
+
指定区间
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
{mode === 'count' ? (
|
|
123
|
+
<>
|
|
124
|
+
<div className={css.field} style={{ flex: 'none', minWidth: 90 }}>
|
|
125
|
+
<label className={css.fieldLabel}>起始章</label>
|
|
126
|
+
<input className={css.input} type="number" min={1} value={startNo} onChange={e => { setStartNo(Math.max(1, Number(e.target.value) || 1)) }} />
|
|
127
|
+
</div>
|
|
128
|
+
<div className={css.field} style={{ flex: 'none', minWidth: 90 }}>
|
|
129
|
+
<label className={css.fieldLabel}>新增章数</label>
|
|
130
|
+
<input className={css.input} type="number" min={1} max={200} value={count} onChange={e => { setCount(Math.max(1, Math.min(200, Number(e.target.value) || 1))) }} />
|
|
131
|
+
</div>
|
|
132
|
+
</>
|
|
133
|
+
) : (
|
|
134
|
+
<>
|
|
135
|
+
<div className={css.field} style={{ flex: 'none', minWidth: 90 }}>
|
|
136
|
+
<label className={css.fieldLabel}>起始章</label>
|
|
137
|
+
<input className={css.input} type="number" min={1} value={startNo} onChange={e => { setStartNo(Math.max(1, Number(e.target.value) || 1)) }} />
|
|
138
|
+
</div>
|
|
139
|
+
<div className={css.field} style={{ flex: 'none', minWidth: 90 }}>
|
|
140
|
+
<label className={css.fieldLabel}>结束章</label>
|
|
141
|
+
<input className={css.input} type="number" min={1} value={endNo} onChange={e => { setEndNo(Math.max(1, Number(e.target.value) || 1)) }} />
|
|
142
|
+
</div>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy || (run?.status === 'running')} onClick={() => { void handleStart() }} title="启动生产单">
|
|
146
|
+
{run?.status === 'running' ? '生产中…' : run?.status === 'paused' ? '▶ 继续' : '▶ 下单生产'}
|
|
147
|
+
</button>
|
|
148
|
+
{run !== null && run.status !== 'done' && (
|
|
149
|
+
<>
|
|
150
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy || run.status !== 'running'} onClick={() => { void handleControl('pause') }}>
|
|
151
|
+
⏸ 暂停
|
|
152
|
+
</button>
|
|
153
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy || run.status === 'stopped'} onClick={() => { void handleControl('stop') }}>
|
|
154
|
+
⏹ 停止
|
|
155
|
+
</button>
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
158
|
+
{run !== null && run.status === 'paused' && (
|
|
159
|
+
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleControl('resume') }}>
|
|
160
|
+
▶ 继续
|
|
161
|
+
</button>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{err !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 12 }}>{err}</div>}
|
|
166
|
+
|
|
167
|
+
{/* 进度区 */}
|
|
168
|
+
{run !== null && (
|
|
169
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
170
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 8 }}>
|
|
171
|
+
<span className={css.meta}>范围:第 {run.startNo} - {run.endNo} 章 · 当前:第 {run.currentNo} 章</span>
|
|
172
|
+
<span className={css.meta}>新生成 {run.stats.generated} · 修订通过 {run.stats.revised} · 豁免 {run.stats.exempted} · 重生成 {run.stats.regenerated} · 失败 {run.stats.error}</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div className={css.bigProgressBar}>
|
|
175
|
+
<div className={css.bigProgressBarFill} style={{ width: `${Math.round(ratio * 100)}%` }} />
|
|
176
|
+
</div>
|
|
177
|
+
{run.pendingManual.length > 0 && (
|
|
178
|
+
<div className={css.meta} style={{ color: 'var(--nf-warn)' }}>
|
|
179
|
+
⚠️ 待人工:第 {run.pendingManual.join('、')} 章(两轮修订仍不过,保留草稿,可在章节列表处理)
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
{run.status === 'done' && <div className={css.meta} style={{ color: 'var(--nf-success)' }}>✅ 生产单完成:{run.startNo}-{run.endNo} 章处理完毕,待人工 {run.pendingManual.length} 章。</div>}
|
|
183
|
+
{run.error !== undefined && <div className={css.meta} style={{ color: 'var(--nf-error)' }}>异常:{run.error}</div>}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* 日志区 */}
|
|
188
|
+
<div className={css.meta} style={{ fontWeight: 600 }}>运行日志({run?.log.length ?? 0})</div>
|
|
189
|
+
<div
|
|
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 }}
|
|
192
|
+
>
|
|
193
|
+
{run === null || run.log.length === 0 ? (
|
|
194
|
+
<span className={css.meta}>尚无日志——下单后这里会实时显示每章进度。</span>
|
|
195
|
+
) : run.log.map((l, i) => (
|
|
196
|
+
<div key={i} style={{ display: 'flex', gap: 6, opacity: 0.85 }}>
|
|
197
|
+
<span style={{ color: 'var(--nf-text-3)', flex: 'none' }}>{new Date(l.at).toLocaleTimeString('zh-CN', { hour12: false })}</span>
|
|
198
|
+
<span>{l.text}</span>
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -178,18 +178,19 @@ export function ShelfView({
|
|
|
178
178
|
value={query}
|
|
179
179
|
onChange={e => { setQuery(e.target.value) }}
|
|
180
180
|
/>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
181
|
+
</div>
|
|
182
|
+
<div className={css.shelfFilters} style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
|
183
|
+
{filters.map(f => (
|
|
184
|
+
<button
|
|
185
|
+
key={f.id}
|
|
186
|
+
type="button"
|
|
187
|
+
className={`${css.button} ${filter === f.id ? css.buttonPrimary : ''}`}
|
|
188
|
+
style={{ fontSize: 14, flex: 1 }}
|
|
189
|
+
onClick={() => { setFilter(f.id) }}
|
|
190
|
+
>
|
|
191
|
+
{f.label}{f.count > 0 ? `(${f.count})` : ''}
|
|
192
|
+
</button>
|
|
193
|
+
))}
|
|
193
194
|
</div>
|
|
194
195
|
</div>
|
|
195
196
|
|
|
@@ -96,8 +96,8 @@ body[data-ds-dark-theme] .entry {
|
|
|
96
96
|
--nf-glass-sheen: rgba(255, 255, 255, 0.55);
|
|
97
97
|
--nf-glass-border: rgba(255, 255, 255, 0.65);
|
|
98
98
|
--nf-glass-inset: rgba(255, 255, 255, 0.55);
|
|
99
|
-
--nf-glass-orb-a: rgba(0, 184, 92, 0.
|
|
100
|
-
--nf-glass-orb-b: rgba(120, 160, 255, 0.
|
|
99
|
+
--nf-glass-orb-a: rgba(0, 184, 92, 0.18);
|
|
100
|
+
--nf-glass-orb-b: rgba(120, 160, 255, 0.13);
|
|
101
101
|
--nf-glass-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
102
102
|
background: var(--nf-bg);
|
|
103
103
|
/* Frosted glass: blur whatever is behind the panel (the chat dialog),
|
|
@@ -138,8 +138,8 @@ body[data-ds-dark-theme] .view {
|
|
|
138
138
|
--nf-glass-sheen: rgba(255, 255, 255, 0.26);
|
|
139
139
|
--nf-glass-border: rgba(255, 255, 255, 0.14);
|
|
140
140
|
--nf-glass-inset: rgba(255, 255, 255, 0.07);
|
|
141
|
-
--nf-glass-orb-a: rgba(0, 202, 82, 0.
|
|
142
|
-
--nf-glass-orb-b: rgba(190, 200, 215, 0.
|
|
141
|
+
--nf-glass-orb-a: rgba(0, 202, 82, 0.28);
|
|
142
|
+
--nf-glass-orb-b: rgba(190, 200, 215, 0.10);
|
|
143
143
|
--nf-glass-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -334,23 +334,6 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
334
334
|
radial-gradient(120% 95% at 50% 42%, transparent 55%, rgba(2, 2, 3, 0.10) 100%);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
.panelHeader {
|
|
338
|
-
display: flex;
|
|
339
|
-
align-items: center;
|
|
340
|
-
justify-content: space-between;
|
|
341
|
-
margin: 10px 12px 0;
|
|
342
|
-
padding: 10px 16px;
|
|
343
|
-
border: 1px solid var(--nf-glass-border);
|
|
344
|
-
border-radius: 18px;
|
|
345
|
-
flex-shrink: 0;
|
|
346
|
-
background: var(--nf-glass-face);
|
|
347
|
-
box-shadow: var(--nf-shadow), inset 0 1px 0 var(--nf-glass-sheen);
|
|
348
|
-
-webkit-backdrop-filter: blur(28px) saturate(175%);
|
|
349
|
-
backdrop-filter: blur(28px) saturate(175%);
|
|
350
|
-
/* 固定在面板顶部(panel 为 flex 列布局,header 正常占位),
|
|
351
|
-
下方内容在 panelBody 内独立滚动,header 永不遮挡内容。 */
|
|
352
|
-
}
|
|
353
|
-
|
|
354
337
|
.panelTitle {
|
|
355
338
|
margin: 0;
|
|
356
339
|
font-size: 16px;
|
|
@@ -456,15 +439,15 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
456
439
|
|
|
457
440
|
.panelNav {
|
|
458
441
|
flex: none;
|
|
459
|
-
align-self:
|
|
460
|
-
width:
|
|
442
|
+
align-self: stretch;
|
|
443
|
+
width: 200px;
|
|
461
444
|
display: flex;
|
|
462
445
|
flex-direction: column;
|
|
463
446
|
gap: 4px;
|
|
464
447
|
margin: 10px 0 10px 12px;
|
|
465
|
-
padding: 10px
|
|
448
|
+
padding: 14px 10px 12px;
|
|
466
449
|
border: 1px solid var(--nf-glass-border);
|
|
467
|
-
border-radius:
|
|
450
|
+
border-radius: 18px;
|
|
468
451
|
background: var(--nf-glass-face);
|
|
469
452
|
box-shadow: var(--nf-shadow), inset 0 1px 0 var(--nf-glass-sheen);
|
|
470
453
|
-webkit-backdrop-filter: blur(28px) saturate(175%);
|
|
@@ -490,7 +473,7 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
490
473
|
letter-spacing: 0.6px;
|
|
491
474
|
text-transform: uppercase;
|
|
492
475
|
color: var(--nf-text-3);
|
|
493
|
-
padding:
|
|
476
|
+
padding: 10px 10px 4px;
|
|
494
477
|
user-select: none;
|
|
495
478
|
}
|
|
496
479
|
|
|
@@ -622,15 +605,15 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
622
605
|
align-items: center;
|
|
623
606
|
gap: 8px;
|
|
624
607
|
width: 100%;
|
|
625
|
-
border:
|
|
608
|
+
border: 1px solid transparent;
|
|
626
609
|
background: transparent;
|
|
627
610
|
cursor: pointer;
|
|
628
|
-
border-radius:
|
|
629
|
-
padding:
|
|
611
|
+
border-radius: 10px;
|
|
612
|
+
padding: 9px 10px;
|
|
630
613
|
font-size: 13px;
|
|
631
614
|
color: var(--nf-text-2-alpha, var(--nf-text-2));
|
|
632
615
|
font-weight: 500;
|
|
633
|
-
transition: color 0.15s ease, background 0.15s ease;
|
|
616
|
+
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
|
634
617
|
position: relative;
|
|
635
618
|
text-align: left;
|
|
636
619
|
white-space: nowrap;
|
|
@@ -649,27 +632,20 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
649
632
|
.navTab[data-active] {
|
|
650
633
|
color: var(--nf-accent);
|
|
651
634
|
font-weight: 600;
|
|
652
|
-
background: var(--nf-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
.navTab[data-active]::before {
|
|
656
|
-
content: '';
|
|
657
|
-
position: absolute;
|
|
658
|
-
left: -8px;
|
|
659
|
-
top: 20%;
|
|
660
|
-
bottom: 20%;
|
|
661
|
-
width: 3px;
|
|
662
|
-
border-radius: 999px;
|
|
663
|
-
background: var(--nf-accent);
|
|
635
|
+
background: color-mix(in srgb, var(--nf-text) 8%, transparent);
|
|
636
|
+
border-color: color-mix(in srgb, var(--nf-text) 16%, transparent);
|
|
637
|
+
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--nf-text) 6%, transparent);
|
|
664
638
|
}
|
|
665
639
|
|
|
666
|
-
.
|
|
667
|
-
|
|
640
|
+
.navTab[data-active] .navTabIcon {
|
|
641
|
+
color: var(--nf-accent);
|
|
668
642
|
}
|
|
669
643
|
|
|
670
644
|
.navTabIcon {
|
|
671
645
|
flex: none;
|
|
672
|
-
|
|
646
|
+
width: 20px;
|
|
647
|
+
text-align: center;
|
|
648
|
+
font-size: 16px;
|
|
673
649
|
line-height: 1;
|
|
674
650
|
}
|
|
675
651
|
|
|
@@ -678,43 +654,183 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
678
654
|
text-overflow: ellipsis;
|
|
679
655
|
}
|
|
680
656
|
|
|
681
|
-
/*
|
|
682
|
-
.
|
|
657
|
+
/* demo 风格:侧栏标题区(Logo + 小说工坊 + 当前书名) */
|
|
658
|
+
.navTitle {
|
|
683
659
|
display: flex;
|
|
684
660
|
align-items: center;
|
|
685
|
-
gap:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
661
|
+
gap: 10px;
|
|
662
|
+
padding: 2px 6px 12px;
|
|
663
|
+
border-bottom: 1px solid var(--nf-border);
|
|
664
|
+
margin-bottom: 6px;
|
|
665
|
+
flex: none;
|
|
689
666
|
min-width: 0;
|
|
690
|
-
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.navTitleLogo {
|
|
670
|
+
width: 34px;
|
|
671
|
+
height: 34px;
|
|
672
|
+
border-radius: 10px;
|
|
673
|
+
background: linear-gradient(145deg, var(--nf-accent), var(--nf-accent-hover));
|
|
674
|
+
color: var(--nf-accent-fg);
|
|
675
|
+
font-weight: 800;
|
|
676
|
+
font-size: 15px;
|
|
677
|
+
display: flex;
|
|
678
|
+
align-items: center;
|
|
679
|
+
justify-content: center;
|
|
680
|
+
flex: none;
|
|
681
|
+
box-shadow: 0 6px 16px var(--nf-accent-soft);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.navTitleName {
|
|
685
|
+
font-weight: 700;
|
|
686
|
+
font-size: 15px;
|
|
687
|
+
letter-spacing: 0.2px;
|
|
688
|
+
white-space: nowrap;
|
|
689
|
+
overflow: hidden;
|
|
690
|
+
text-overflow: ellipsis;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.navTitleBook {
|
|
694
|
+
font-size: 11px;
|
|
695
|
+
color: var(--nf-text-3);
|
|
691
696
|
white-space: nowrap;
|
|
697
|
+
overflow: hidden;
|
|
698
|
+
text-overflow: ellipsis;
|
|
692
699
|
}
|
|
693
700
|
|
|
694
|
-
.
|
|
695
|
-
|
|
696
|
-
|
|
701
|
+
.panelNavCollapsed .navTitle {
|
|
702
|
+
justify-content: center;
|
|
703
|
+
padding: 2px 0 10px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.panelNavCollapsed .navTitleLogo {
|
|
707
|
+
width: 32px;
|
|
708
|
+
height: 32px;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* 操作行:收起导航 / 关闭面板 */
|
|
712
|
+
.navActions {
|
|
713
|
+
flex: none;
|
|
714
|
+
display: flex;
|
|
715
|
+
gap: 6px;
|
|
716
|
+
margin-top: 8px;
|
|
717
|
+
padding-top: 8px;
|
|
718
|
+
border-top: 1px solid var(--nf-border);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.navActionBtn {
|
|
722
|
+
flex: 1;
|
|
723
|
+
border: 1px solid var(--nf-border);
|
|
724
|
+
background: rgba(0, 0, 0, 0);
|
|
697
725
|
color: var(--nf-text-2);
|
|
726
|
+
cursor: pointer;
|
|
727
|
+
border-radius: 10px;
|
|
728
|
+
height: 32px;
|
|
729
|
+
font-size: 14px;
|
|
730
|
+
line-height: 1;
|
|
698
731
|
display: inline-flex;
|
|
699
732
|
align-items: center;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
white-space: nowrap;
|
|
733
|
+
justify-content: center;
|
|
734
|
+
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
703
735
|
}
|
|
704
736
|
|
|
705
|
-
.
|
|
706
|
-
|
|
737
|
+
.navActionBtn:hover {
|
|
738
|
+
background: var(--nf-hover);
|
|
739
|
+
color: var(--nf-text);
|
|
740
|
+
border-color: var(--nf-border-strong);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.panelNavCollapsed .navActions {
|
|
744
|
+
flex-direction: column;
|
|
745
|
+
align-items: stretch;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/* demo 风格:底部当前书切换卡(封面首字 + 书名/进度 + 箭头) */
|
|
749
|
+
.bookSwitch {
|
|
750
|
+
flex: none;
|
|
751
|
+
display: flex;
|
|
752
|
+
align-items: center;
|
|
753
|
+
gap: 8px;
|
|
754
|
+
margin-top: 4px;
|
|
755
|
+
padding: 9px;
|
|
756
|
+
border-radius: 12px;
|
|
757
|
+
background: var(--nf-bg-inset);
|
|
758
|
+
border: 1px solid var(--nf-border);
|
|
759
|
+
cursor: pointer;
|
|
760
|
+
text-align: left;
|
|
761
|
+
font: inherit;
|
|
762
|
+
color: var(--nf-text);
|
|
763
|
+
min-width: 0;
|
|
764
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.bookSwitch:hover {
|
|
768
|
+
background: var(--nf-hover);
|
|
769
|
+
border-color: var(--nf-border-strong);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.bookSwitchCover {
|
|
773
|
+
width: 26px;
|
|
774
|
+
height: 34px;
|
|
775
|
+
border-radius: 5px;
|
|
776
|
+
background: linear-gradient(160deg, var(--nf-accent), color-mix(in srgb, var(--nf-accent) 45%, #000));
|
|
777
|
+
color: var(--nf-accent-fg);
|
|
778
|
+
display: flex;
|
|
779
|
+
align-items: center;
|
|
780
|
+
justify-content: center;
|
|
781
|
+
font-size: 11px;
|
|
707
782
|
font-weight: 700;
|
|
783
|
+
flex: none;
|
|
708
784
|
}
|
|
709
785
|
|
|
710
|
-
.
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
786
|
+
.bookSwitchName {
|
|
787
|
+
font-size: 12.5px;
|
|
788
|
+
font-weight: 600;
|
|
789
|
+
overflow: hidden;
|
|
790
|
+
text-overflow: ellipsis;
|
|
791
|
+
white-space: nowrap;
|
|
716
792
|
}
|
|
717
793
|
|
|
794
|
+
.bookSwitchMeta {
|
|
795
|
+
font-size: 11px;
|
|
796
|
+
color: var(--nf-text-3);
|
|
797
|
+
overflow: hidden;
|
|
798
|
+
text-overflow: ellipsis;
|
|
799
|
+
white-space: nowrap;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.bookSwitchArrow {
|
|
803
|
+
margin-left: auto;
|
|
804
|
+
color: var(--nf-text-3);
|
|
805
|
+
font-size: 12px;
|
|
806
|
+
flex: none;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/* demo 风格统计卡(今日待写 / 写作中 / 已通过 / 待审稿) */
|
|
810
|
+
.statRowD { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
|
|
811
|
+
.statCardD { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; padding: 14px 16px; border-radius: 16px; background: var(--nf-bg-inset); border: 1px solid var(--nf-border); min-width: 0; }
|
|
812
|
+
.statCardDLabel { font-size: 12px; color: var(--nf-text-3); }
|
|
813
|
+
.statCardDValue { font-size: 22px; font-weight: 700; margin-top: 2px; color: var(--nf-text); }
|
|
814
|
+
.statCardDUnit { font-size: 12px; color: var(--nf-text-3); font-weight: 400; }
|
|
815
|
+
.statCardDDetail { font-size: 11.5px; color: var(--nf-text-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
816
|
+
.statCardDDown { color: var(--nf-error); }
|
|
817
|
+
.statCardDIcon { width: 38px; height: 38px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex: none; background: var(--nf-accent-soft); }
|
|
818
|
+
.statCardDIconRed { background: color-mix(in srgb, var(--nf-error) 13%, transparent); }
|
|
819
|
+
|
|
820
|
+
/* demo 风格创作时间线(6 阶段分段条,点击跳转对应 tab) */
|
|
821
|
+
.tlBar { display: flex; gap: 6px; margin-top: 8px; }
|
|
822
|
+
.tlSeg { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; border: none; background: transparent; padding: 0; cursor: pointer; font: inherit; }
|
|
823
|
+
.tlSegTrack { height: 8px; border-radius: 999px; background: var(--nf-hover); border: 1px solid var(--nf-border); overflow: hidden; }
|
|
824
|
+
.tlSegFill { display: block; height: 100%; width: 100%; border-radius: 999px; background: var(--nf-accent); }
|
|
825
|
+
.tlSegTodo .tlSegFill { opacity: 0; }
|
|
826
|
+
.tlSegTodo .tlSegTrack { background: transparent; }
|
|
827
|
+
.tlSegCurrent .tlSegTrack { border-color: var(--nf-accent); box-shadow: 0 0 0 2px var(--nf-accent-soft); }
|
|
828
|
+
.tlSegCurrent .tlSegFill { opacity: 0.45; }
|
|
829
|
+
.tlSegLabel { font-size: 10.5px; color: var(--nf-text-3); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
830
|
+
.tlSegDone .tlSegLabel { color: var(--nf-accent); font-weight: 600; }
|
|
831
|
+
.tlSeg:hover .tlSegTrack { border-color: var(--nf-border-strong); }
|
|
832
|
+
|
|
833
|
+
|
|
718
834
|
/* ------------------------------------------------------------ cards */
|
|
719
835
|
|
|
720
836
|
.card {
|
|
@@ -1592,8 +1708,16 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
1592
1708
|
display: flex;
|
|
1593
1709
|
flex-direction: column;
|
|
1594
1710
|
gap: 4px;
|
|
1595
|
-
background: var(--nf-glass-
|
|
1596
|
-
box-shadow: inset 0 1px 0 var(--nf-glass-sheen);
|
|
1711
|
+
background: var(--nf-glass-face);
|
|
1712
|
+
box-shadow: var(--nf-card-shadow), inset 0 1px 0 var(--nf-glass-sheen);
|
|
1713
|
+
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
|
1714
|
+
backdrop-filter: blur(30px) saturate(180%);
|
|
1715
|
+
transition: transform 0.22s ease, box-shadow 0.22s ease;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
.assetStat:hover {
|
|
1719
|
+
transform: translateY(-3px);
|
|
1720
|
+
box-shadow: var(--nf-card-shadow-hover), inset 0 1px 0 var(--nf-glass-sheen);
|
|
1597
1721
|
}
|
|
1598
1722
|
|
|
1599
1723
|
.assetStatLabel {
|