@waterwx/dsh-novel-forge 1.4.0 → 1.4.1
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 +825 -564
- package/lib/client.js.map +1 -1
- package/lib/index.js +330 -58
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +7 -1
- package/lib/types/client/locales.d.ts +4 -2
- package/lib/types/engine.d.ts +27 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/protocol.d.ts +45 -3
- package/package.json +1 -1
- package/src/client/api.ts +16 -1
- package/src/client/locales.ts +7 -3
- package/src/client/panel/ImportModal.tsx +123 -21
- package/src/client/panel/NovelPanel.tsx +221 -147
- package/src/client/panel/panel.module.css +77 -0
- package/src/engine.ts +210 -39
- package/src/index.ts +6 -0
- package/src/protocol.ts +36 -3
- package/src/routes.ts +88 -11
|
@@ -791,6 +791,30 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
791
791
|
}
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
+
/** 反推大纲:从已写章节正文反向生成全书总纲(流式进度)。 */
|
|
795
|
+
const handleReverseOutline = async (): Promise<void> => {
|
|
796
|
+
if (project === null) return
|
|
797
|
+
if (!window.confirm('将从已写章节正文反推出全书总纲(覆盖当前大纲文本,不影响章节/设定/进度)。确定继续?')) return
|
|
798
|
+
setBusy(true)
|
|
799
|
+
setError('')
|
|
800
|
+
try {
|
|
801
|
+
await api.outlineReverse(frame => {
|
|
802
|
+
if (frame.type === 'outline-progress') {
|
|
803
|
+
pushProgress(`反推大纲:${frame.phase}(${frame.done}/${frame.total})`, 'info')
|
|
804
|
+
} else if (frame.type === 'outline-done') {
|
|
805
|
+
setOutlineText(frame.outline)
|
|
806
|
+
pushProgress(`反推大纲完成(${frame.chars} 字),已保存为当前总纲`, 'done')
|
|
807
|
+
}
|
|
808
|
+
})
|
|
809
|
+
await refresh(false)
|
|
810
|
+
} catch (err) {
|
|
811
|
+
setError((err as Error).message)
|
|
812
|
+
pushProgress(`反推大纲失败:${(err as Error).message}`, 'error')
|
|
813
|
+
} finally {
|
|
814
|
+
setBusy(false)
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
794
818
|
/** 大纲页「更新大纲」展开/收起(展开时预填当前大纲文本)。 */
|
|
795
819
|
const handleToggleUpdateOutline = (): void => {
|
|
796
820
|
if (!updatingOutline && project !== null) setOutlineText(project.outline)
|
|
@@ -3001,6 +3025,15 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
3001
3025
|
{tt('overview.bookName')}: {project.bookName} · {project.outline.length} 字
|
|
3002
3026
|
{project.outlinePath !== undefined && <span> · {project.outlinePath}</span>}
|
|
3003
3027
|
</span>
|
|
3028
|
+
<button
|
|
3029
|
+
type="button"
|
|
3030
|
+
className={`${css.button} ${css.buttonPrimary}`}
|
|
3031
|
+
disabled={busy || !(project.chapters ?? []).some(c => c.status === 'written' || c.status === 'approved' || c.status === 'rejected')}
|
|
3032
|
+
onClick={() => { void handleReverseOutline() }}
|
|
3033
|
+
title="从已写章节正文反向生成全书总纲(覆盖当前大纲文本)"
|
|
3034
|
+
>
|
|
3035
|
+
🔄 反推大纲
|
|
3036
|
+
</button>
|
|
3004
3037
|
<button type="button" className={css.button} disabled={busy} onClick={() => { handleToggleUpdateOutline() }}>
|
|
3005
3038
|
{updatingOutline ? '收起' : '更新大纲'}
|
|
3006
3039
|
</button>
|
|
@@ -3076,7 +3109,12 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
3076
3109
|
</>
|
|
3077
3110
|
) : (
|
|
3078
3111
|
/* 只读展示:大纲是开书时的出生证明 */
|
|
3112
|
+
<>
|
|
3113
|
+
<div className={css.meta} style={{ marginBottom: 6 }}>
|
|
3114
|
+
大纲是本书的「出生证明」;已写章节可点右上角「🔄 反推大纲」从正文反推,或「更新大纲」手动修订。
|
|
3115
|
+
</div>
|
|
3079
3116
|
<pre className={css.outlineReadonly}>{project.outline}</pre>
|
|
3117
|
+
</>
|
|
3080
3118
|
)}
|
|
3081
3119
|
</div>
|
|
3082
3120
|
<div className={css.card}>
|
|
@@ -3729,170 +3767,206 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
3729
3767
|
{activeTab === 'assets' && <AssetsTab api={api} />}
|
|
3730
3768
|
|
|
3731
3769
|
{activeTab === 'settings' && configDraft !== null && (
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
<div className={css.
|
|
3735
|
-
<
|
|
3736
|
-
<
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
<label className={css.fieldLabel}>{tt('settings.outputDir')}</label>
|
|
3740
|
-
<input className={css.input} value={configDraft.outputDir} onChange={e => { setConfigDraft({ ...configDraft, outputDir: e.target.value }) }} />
|
|
3741
|
-
</div>
|
|
3742
|
-
<div className={css.row} style={{ alignItems: 'flex-start' }}>
|
|
3743
|
-
<div className={css.field} style={{ flex: 1 }}>
|
|
3744
|
-
<label className={css.fieldLabel}>{tt('settings.provider')}</label>
|
|
3745
|
-
<input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
|
|
3770
|
+
<>
|
|
3771
|
+
{/* ① 路径与输出 */}
|
|
3772
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3773
|
+
<span className={css.cardTitle}>📁 路径与输出</span>
|
|
3774
|
+
<div className={css.field}>
|
|
3775
|
+
<label className={css.fieldLabel}>{tt('settings.outlinePath')}</label>
|
|
3776
|
+
<input className={css.input} value={configDraft.outlinePath} onChange={e => { setConfigDraft({ ...configDraft, outlinePath: e.target.value }) }} />
|
|
3746
3777
|
</div>
|
|
3747
|
-
<div className={css.field}
|
|
3748
|
-
<label className={css.fieldLabel}>{tt('settings.
|
|
3749
|
-
<
|
|
3750
|
-
className={css.input}
|
|
3751
|
-
value={modelCustomMode ? '__custom__' : configDraft.model}
|
|
3752
|
-
onChange={e => {
|
|
3753
|
-
const v = e.target.value
|
|
3754
|
-
if (v === '__custom__') {
|
|
3755
|
-
setModelCustomMode(true)
|
|
3756
|
-
} else {
|
|
3757
|
-
setModelCustomMode(false)
|
|
3758
|
-
setConfigDraft({ ...configDraft, model: v })
|
|
3759
|
-
}
|
|
3760
|
-
}}
|
|
3761
|
-
>
|
|
3762
|
-
{MODEL_PRESETS.map(m => <option key={m} value={m}>{m}</option>)}
|
|
3763
|
-
<option value="__custom__">{tt('settings.modelCustom')}</option>
|
|
3764
|
-
</select>
|
|
3765
|
-
{modelCustomMode && (
|
|
3766
|
-
<input
|
|
3767
|
-
className={css.input}
|
|
3768
|
-
style={{ marginTop: 6 }}
|
|
3769
|
-
value={configDraft.model}
|
|
3770
|
-
placeholder={tt('settings.modelCustomPlaceholder')}
|
|
3771
|
-
onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
|
|
3772
|
-
/>
|
|
3773
|
-
)}
|
|
3778
|
+
<div className={css.field}>
|
|
3779
|
+
<label className={css.fieldLabel}>{tt('settings.outputDir')}</label>
|
|
3780
|
+
<input className={css.input} value={configDraft.outputDir} onChange={e => { setConfigDraft({ ...configDraft, outputDir: e.target.value }) }} />
|
|
3774
3781
|
</div>
|
|
3775
|
-
<div className={css.
|
|
3776
|
-
<
|
|
3777
|
-
<select
|
|
3778
|
-
className={css.input}
|
|
3779
|
-
value={configDraft.reasoningEffort ?? 'off'}
|
|
3780
|
-
onChange={e => { setConfigDraft({ ...configDraft, reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] }) }}
|
|
3781
|
-
>
|
|
3782
|
-
{REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
|
|
3783
|
-
</select>
|
|
3784
|
-
<span className={css.meta}>{tt('settings.reasoningHint')}</span>
|
|
3782
|
+
<div className={css.row}>
|
|
3783
|
+
<button type="button" className={css.button} onClick={() => { void api.openFolder() }}>{tt('settings.openFolder')}</button>
|
|
3785
3784
|
</div>
|
|
3786
3785
|
</div>
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3786
|
+
|
|
3787
|
+
{/* ② 模型与推理 */}
|
|
3788
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3789
|
+
<span className={css.cardTitle}>🤖 模型与推理</span>
|
|
3790
|
+
<div className={css.row} style={{ alignItems: 'flex-start' }}>
|
|
3791
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3792
|
+
<label className={css.fieldLabel}>{tt('settings.provider')}</label>
|
|
3793
|
+
<input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
|
|
3794
|
+
</div>
|
|
3795
|
+
<div className={css.field} style={{ flex: 1.4 }}>
|
|
3796
|
+
<label className={css.fieldLabel}>{tt('settings.model')}</label>
|
|
3797
|
+
<select
|
|
3798
|
+
className={css.input}
|
|
3799
|
+
value={modelCustomMode ? '__custom__' : configDraft.model}
|
|
3800
|
+
onChange={e => {
|
|
3801
|
+
const v = e.target.value
|
|
3802
|
+
if (v === '__custom__') {
|
|
3803
|
+
setModelCustomMode(true)
|
|
3804
|
+
} else {
|
|
3805
|
+
setModelCustomMode(false)
|
|
3806
|
+
setConfigDraft({ ...configDraft, model: v })
|
|
3807
|
+
}
|
|
3808
|
+
}}
|
|
3809
|
+
>
|
|
3810
|
+
{MODEL_PRESETS.map(m => <option key={m} value={m}>{m}</option>)}
|
|
3811
|
+
<option value="__custom__">{tt('settings.modelCustom')}</option>
|
|
3812
|
+
</select>
|
|
3813
|
+
{modelCustomMode && (
|
|
3814
|
+
<input
|
|
3815
|
+
className={css.input}
|
|
3816
|
+
style={{ marginTop: 6 }}
|
|
3817
|
+
value={configDraft.model}
|
|
3818
|
+
placeholder={tt('settings.modelCustomPlaceholder')}
|
|
3819
|
+
onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
|
|
3820
|
+
/>
|
|
3821
|
+
)}
|
|
3822
|
+
</div>
|
|
3795
3823
|
</div>
|
|
3796
|
-
<div className={css.
|
|
3797
|
-
<
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3824
|
+
<div className={css.row} style={{ alignItems: 'flex-start' }}>
|
|
3825
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3826
|
+
<label className={css.fieldLabel}>{tt('settings.reasoningEffort')}</label>
|
|
3827
|
+
<select
|
|
3828
|
+
className={css.input}
|
|
3829
|
+
value={configDraft.reasoningEffort ?? 'off'}
|
|
3830
|
+
onChange={e => { setConfigDraft({ ...configDraft, reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] }) }}
|
|
3831
|
+
>
|
|
3832
|
+
{REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
|
|
3833
|
+
</select>
|
|
3834
|
+
<span className={css.meta}>{tt('settings.reasoningHint')}</span>
|
|
3835
|
+
</div>
|
|
3836
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3837
|
+
<label className={css.fieldLabel}>{tt('settings.analysisReasoning')}</label>
|
|
3838
|
+
<select
|
|
3839
|
+
className={css.input}
|
|
3840
|
+
value={configDraft.analysisReasoning ?? 'low'}
|
|
3841
|
+
onChange={e => { setConfigDraft({ ...configDraft, analysisReasoning: e.target.value as NovelConfig['analysisReasoning'] }) }}
|
|
3842
|
+
>
|
|
3843
|
+
{REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
|
|
3844
|
+
</select>
|
|
3845
|
+
<span className={css.meta}>{tt('settings.analysisReasoningHint')}</span>
|
|
3846
|
+
</div>
|
|
3807
3847
|
</div>
|
|
3808
3848
|
</div>
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3849
|
+
|
|
3850
|
+
{/* ③ 生图(豆包) */}
|
|
3851
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3852
|
+
<span className={css.cardTitle}>🎨 生图(豆包)</span>
|
|
3853
|
+
<div className={css.row}>
|
|
3854
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3855
|
+
<label className={css.fieldLabel}>豆包 API Key(生图)</label>
|
|
3856
|
+
<input className={css.input} type="password" placeholder="ark-..." value={configDraft.imageApiKey ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiKey: e.target.value }) }} />
|
|
3857
|
+
</div>
|
|
3858
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3859
|
+
<label className={css.fieldLabel}>豆包生图模型</label>
|
|
3860
|
+
<input className={css.input} placeholder="doubao-seedream-5-0-pro-260628" value={configDraft.imageApiModel ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiModel: e.target.value }) }} />
|
|
3861
|
+
</div>
|
|
3862
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3863
|
+
<label className={css.fieldLabel}>启用豆包生图</label>
|
|
3864
|
+
<select
|
|
3865
|
+
className={css.input}
|
|
3866
|
+
value={configDraft.imageApiEnabled ? '1' : '0'}
|
|
3867
|
+
onChange={e => { setConfigDraft({ ...configDraft, imageApiEnabled: e.target.value === '1' }) }}
|
|
3868
|
+
>
|
|
3869
|
+
<option value="0">关(默认)</option>
|
|
3870
|
+
<option value="1">开</option>
|
|
3871
|
+
</select>
|
|
3872
|
+
<span className={css.meta}>开启后角色详情显示「豆包生成」参考图按钮</span>
|
|
3873
|
+
</div>
|
|
3817
3874
|
</div>
|
|
3818
3875
|
</div>
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
)
|
|
3834
|
-
|
|
3835
|
-
|
|
3876
|
+
|
|
3877
|
+
{/* ④ 写作与审稿参数 */}
|
|
3878
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3879
|
+
<span className={css.cardTitle}>✍️ 写作与审稿参数</span>
|
|
3880
|
+
<div className={css.row}>
|
|
3881
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3882
|
+
<label className={css.fieldLabel}>{tt('settings.chapterChars')}</label>
|
|
3883
|
+
<input className={css.input} type="number" min={1000} max={20000} value={configDraft.chapterChars} onChange={e => { setConfigDraft({ ...configDraft, chapterChars: Number(e.target.value) }) }} />
|
|
3884
|
+
</div>
|
|
3885
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3886
|
+
<label className={css.fieldLabel}>{tt('settings.maxTokens')}</label>
|
|
3887
|
+
<input className={css.input} type="number" min={2000} max={64000} value={configDraft.maxTokens} onChange={e => { setConfigDraft({ ...configDraft, maxTokens: Number(e.target.value) }) }} />
|
|
3888
|
+
</div>
|
|
3889
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3890
|
+
<label className={css.fieldLabel}>{tt('settings.editorFontSize')}</label>
|
|
3891
|
+
<select
|
|
3892
|
+
className={css.input}
|
|
3893
|
+
value={editorFontSize}
|
|
3894
|
+
onChange={e => { changeEditorFontSize(Number(e.target.value)) }}
|
|
3895
|
+
>
|
|
3896
|
+
{[12, 13, 14, 15, 16, 18, 20, 22, 24].map(v => (
|
|
3897
|
+
<option key={v} value={v}>{v}px</option>
|
|
3898
|
+
))}
|
|
3899
|
+
</select>
|
|
3900
|
+
<span className={css.meta}>{tt('settings.editorFontSizeHint')}</span>
|
|
3901
|
+
</div>
|
|
3836
3902
|
</div>
|
|
3837
|
-
<div className={css.
|
|
3838
|
-
<
|
|
3839
|
-
|
|
3840
|
-
className={css.input}
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
>
|
|
3844
|
-
<
|
|
3845
|
-
<
|
|
3846
|
-
|
|
3903
|
+
<div className={css.row}>
|
|
3904
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3905
|
+
<label className={css.fieldLabel}>{tt('settings.reviewPassScore')}</label>
|
|
3906
|
+
<input className={css.input} type="number" min={0} max={100} value={configDraft.reviewPassScore} onChange={e => { setConfigDraft({ ...configDraft, reviewPassScore: Number(e.target.value) }) }} />
|
|
3907
|
+
<span className={css.meta}>通过判定:综合评分 ≥ 此分数(默认 70),或无 high 级问题且评分 ≥ 60;建议设 60-80。</span>
|
|
3908
|
+
</div>
|
|
3909
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3910
|
+
<label className={css.fieldLabel}>{tt('settings.autoReview')}</label>
|
|
3911
|
+
<select
|
|
3912
|
+
className={css.input}
|
|
3913
|
+
value={configDraft.autoReview ? '1' : '0'}
|
|
3914
|
+
onChange={e => { setConfigDraft({ ...configDraft, autoReview: e.target.value === '1' }) }}
|
|
3915
|
+
>
|
|
3916
|
+
<option value="1">✓ 是</option>
|
|
3917
|
+
<option value="0">✗ 否</option>
|
|
3918
|
+
</select>
|
|
3919
|
+
</div>
|
|
3920
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3921
|
+
<label className={css.fieldLabel}>{tt('settings.autoAuthorReview')}</label>
|
|
3922
|
+
<select
|
|
3923
|
+
className={css.input}
|
|
3924
|
+
value={configDraft.autoAuthorReview ? '1' : '0'}
|
|
3925
|
+
onChange={e => { setConfigDraft({ ...configDraft, autoAuthorReview: e.target.value === '1' }) }}
|
|
3926
|
+
>
|
|
3927
|
+
<option value="1">✓ 是</option>
|
|
3928
|
+
<option value="0">✗ 否</option>
|
|
3929
|
+
</select>
|
|
3930
|
+
<span className={css.meta}>{tt('settings.autoAuthorReviewHint')}</span>
|
|
3931
|
+
</div>
|
|
3932
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3933
|
+
<label className={css.fieldLabel}>{tt('settings.autoReviewAfterRevise')}</label>
|
|
3934
|
+
<select
|
|
3935
|
+
className={css.input}
|
|
3936
|
+
value={configDraft.autoReviewAfterRevise ? '1' : '0'}
|
|
3937
|
+
onChange={e => { setConfigDraft({ ...configDraft, autoReviewAfterRevise: e.target.value === '1' }) }}
|
|
3938
|
+
>
|
|
3939
|
+
<option value="1">✓ 是</option>
|
|
3940
|
+
<option value="0">✗ 否</option>
|
|
3941
|
+
</select>
|
|
3942
|
+
<span className={css.meta}>{tt('settings.autoReviewAfterReviseHint')}</span>
|
|
3943
|
+
</div>
|
|
3847
3944
|
</div>
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
</select>
|
|
3858
|
-
<span className={css.meta}>{tt('settings.autoAuthorReviewHint')}</span>
|
|
3945
|
+
</div>
|
|
3946
|
+
|
|
3947
|
+
{/* ⑤ 保存与导出 */}
|
|
3948
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3949
|
+
<div className={css.row}>
|
|
3950
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleSaveConfig() }}>
|
|
3951
|
+
{tt('settings.save')}
|
|
3952
|
+
</button>
|
|
3953
|
+
<span className={css.meta}>当前:{config?.provider} / {config?.model} · {config?.outputDir}</span>
|
|
3859
3954
|
</div>
|
|
3860
|
-
<div className={css.
|
|
3861
|
-
<
|
|
3862
|
-
<
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
<option value="0">✗ 否</option>
|
|
3869
|
-
</select>
|
|
3870
|
-
<span className={css.meta}>{tt('settings.autoReviewAfterReviseHint')}</span>
|
|
3955
|
+
<div className={css.row}>
|
|
3956
|
+
<span className={css.cardTitle}>{tt('settings.export')}</span>
|
|
3957
|
+
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('txt') }}>
|
|
3958
|
+
{tt('settings.exportTxt')}
|
|
3959
|
+
</button>
|
|
3960
|
+
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('md') }}>
|
|
3961
|
+
{tt('settings.exportMd')}
|
|
3962
|
+
</button>
|
|
3871
3963
|
</div>
|
|
3872
3964
|
</div>
|
|
3873
|
-
|
|
3874
|
-
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleSaveConfig() }}>
|
|
3875
|
-
{tt('settings.save')}
|
|
3876
|
-
</button>
|
|
3877
|
-
<button type="button" className={css.button} onClick={() => { void api.openFolder() }}>
|
|
3878
|
-
{tt('settings.openFolder')}
|
|
3879
|
-
</button>
|
|
3880
|
-
<span className={css.meta}>当前:{config?.provider} / {config?.model} · {config?.outputDir}</span>
|
|
3881
|
-
</div>
|
|
3882
|
-
<div className={css.row}>
|
|
3883
|
-
<span className={css.cardTitle}>{tt('settings.export')}</span>
|
|
3884
|
-
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('txt') }}>
|
|
3885
|
-
{tt('settings.exportTxt')}
|
|
3886
|
-
</button>
|
|
3887
|
-
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('md') }}>
|
|
3888
|
-
{tt('settings.exportMd')}
|
|
3889
|
-
</button>
|
|
3890
|
-
</div>
|
|
3891
|
-
</div>
|
|
3965
|
+
</>
|
|
3892
3966
|
)}
|
|
3893
3967
|
|
|
3894
3968
|
{activeTab === 'settings' && (
|
|
3895
|
-
<div className={css.card}>
|
|
3969
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3896
3970
|
<span className={css.cardTitle}>{tt('settings.theme')}</span>
|
|
3897
3971
|
<div className={css.row} style={{ gap: 8, flexWrap: 'wrap' }}>
|
|
3898
3972
|
<button
|
|
@@ -3035,3 +3035,80 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
|
|
|
3035
3035
|
justify-content: flex-end;
|
|
3036
3036
|
gap: 8px;
|
|
3037
3037
|
}
|
|
3038
|
+
|
|
3039
|
+
.importFileInfo {
|
|
3040
|
+
display: flex;
|
|
3041
|
+
align-items: center;
|
|
3042
|
+
gap: 8px;
|
|
3043
|
+
font-size: 13px;
|
|
3044
|
+
color: var(--nf-text-1);
|
|
3045
|
+
flex-wrap: wrap;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
.importPreview {
|
|
3049
|
+
display: flex;
|
|
3050
|
+
flex-direction: column;
|
|
3051
|
+
gap: 4px;
|
|
3052
|
+
border: 1px solid var(--nf-border-strong);
|
|
3053
|
+
border-radius: 10px;
|
|
3054
|
+
padding: 10px 12px;
|
|
3055
|
+
font-size: 13px;
|
|
3056
|
+
background: var(--nf-bg-inset);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
.importPreviewList {
|
|
3060
|
+
margin: 0;
|
|
3061
|
+
padding-left: 18px;
|
|
3062
|
+
display: flex;
|
|
3063
|
+
flex-direction: column;
|
|
3064
|
+
gap: 2px;
|
|
3065
|
+
font-size: 12.5px;
|
|
3066
|
+
color: var(--nf-text-2);
|
|
3067
|
+
max-height: 140px;
|
|
3068
|
+
overflow: auto;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
/* 设置页卡片:label 单行、行内顶部对齐,保证输入框水平对齐 */
|
|
3072
|
+
.settingsCard .fieldLabel {
|
|
3073
|
+
min-height: 18px;
|
|
3074
|
+
line-height: 18px;
|
|
3075
|
+
white-space: nowrap;
|
|
3076
|
+
overflow: hidden;
|
|
3077
|
+
text-overflow: ellipsis;
|
|
3078
|
+
max-width: 100%;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
.settingsCard .row {
|
|
3082
|
+
align-items: flex-start;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
|
|
3086
|
+
|
|
3087
|
+
.helpTipText {
|
|
3088
|
+
display: none;
|
|
3089
|
+
position: absolute;
|
|
3090
|
+
left: 50%;
|
|
3091
|
+
bottom: calc(100% + 8px);
|
|
3092
|
+
transform: translateX(-50%);
|
|
3093
|
+
width: 280px;
|
|
3094
|
+
max-width: 70vw;
|
|
3095
|
+
padding: 10px 12px;
|
|
3096
|
+
border-radius: 10px;
|
|
3097
|
+
background: var(--nf-bg-raise);
|
|
3098
|
+
border: 1px solid var(--nf-border-strong);
|
|
3099
|
+
box-shadow: var(--nf-shadow-lg);
|
|
3100
|
+
font-size: 12px;
|
|
3101
|
+
font-weight: 400;
|
|
3102
|
+
line-height: 1.7;
|
|
3103
|
+
color: var(--nf-text-2);
|
|
3104
|
+
white-space: normal;
|
|
3105
|
+
z-index: 20;
|
|
3106
|
+
text-align: left;
|
|
3107
|
+
pointer-events: none;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
.helpTip:hover .helpTipText,
|
|
3111
|
+
.helpTip:focus .helpTipText,
|
|
3112
|
+
.helpTip:focus-within .helpTipText {
|
|
3113
|
+
display: block;
|
|
3114
|
+
}
|