@waterwx/dsh-novel-forge 1.6.0 → 1.7.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.
@@ -359,13 +359,14 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
359
359
  font-size: 16px;
360
360
  color: var(--nf-text-2);
361
361
  padding: 4px 9px;
362
- border-radius: 8px;
363
- transition: background 0.15s ease, color 0.15s ease;
362
+ border-radius: 9px;
363
+ transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
364
364
  }
365
365
 
366
366
  .iconButton:hover {
367
367
  background: var(--nf-hover);
368
368
  color: var(--nf-text);
369
+ transform: translateY(-1px);
369
370
  }
370
371
 
371
372
  /* ------------------------------------------------------------ tab bar */
@@ -419,10 +420,10 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
419
420
  .panelContent {
420
421
  flex: 1;
421
422
  overflow: auto;
422
- padding: 16px 18px 20px;
423
+ padding: var(--nf-density-pad, 16px 18px 20px);
423
424
  display: flex;
424
425
  flex-direction: column;
425
- gap: 14px;
426
+ gap: var(--nf-density-gap, 14px);
426
427
  position: relative;
427
428
  z-index: 1;
428
429
  }
@@ -445,7 +446,7 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
445
446
  flex-direction: column;
446
447
  gap: 6px;
447
448
  margin: 10px 0 10px 12px;
448
- padding: 16px 12px 14px;
449
+ padding: var(--nf-density-nav, 16px 12px 14px);
449
450
  border: 1px solid var(--nf-glass-border);
450
451
  border-radius: 18px;
451
452
  background: var(--nf-glass-face);
@@ -589,7 +590,7 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
589
590
  background: var(--nf-success);
590
591
  }
591
592
 
592
- /* 任务进行中:工作进度导航呼吸绿点(不自动弹窗,点击导航查看悬浮窗) */
593
+ /* 任务进行中:AI进度导航呼吸绿点(不自动弹窗,点击导航查看悬浮窗) */
593
594
  .navTabBadgeLive {
594
595
  background: var(--nf-success);
595
596
  animation: navBadgePulse 1.2s ease-in-out infinite;
@@ -837,7 +838,7 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
837
838
  position: relative;
838
839
  border: 1px solid var(--nf-glass-border);
839
840
  border-radius: 22px;
840
- padding: 16px 18px;
841
+ padding: var(--nf-density-card, 16px 18px);
841
842
  display: flex;
842
843
  flex-direction: column;
843
844
  gap: 10px;
@@ -906,11 +907,12 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
906
907
  border: 1px solid var(--nf-border-strong);
907
908
  background: var(--nf-bg-inset);
908
909
  color: var(--nf-text);
909
- padding: 6px 14px;
910
- border-radius: 12px;
910
+ padding: 7px 15px;
911
+ border-radius: 9px;
911
912
  cursor: pointer;
912
913
  font-size: 13px;
913
914
  font-weight: 500;
915
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09);
914
916
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease;
915
917
  white-space: nowrap;
916
918
  }
@@ -918,10 +920,11 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
918
920
  .button:hover:not([disabled]) {
919
921
  background: var(--nf-hover);
920
922
  border-color: var(--nf-border-strong);
923
+ transform: translateY(-1px);
921
924
  }
922
925
 
923
926
  .button:active:not([disabled]) {
924
- transform: scale(0.96);
927
+ transform: translateY(0) scale(0.97);
925
928
  }
926
929
 
927
930
  .button[disabled] {
@@ -933,12 +936,15 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
933
936
  border-color: transparent;
934
937
  background: linear-gradient(180deg, color-mix(in srgb, var(--nf-accent) 88%, #ffffff), var(--nf-accent));
935
938
  color: var(--nf-accent-fg);
936
- box-shadow: 0 2px 10px color-mix(in srgb, var(--nf-accent) 45%, transparent), 0 0 0 0.5px color-mix(in srgb, var(--nf-accent) 30%, transparent);
939
+ border-radius: 10px;
940
+ box-shadow: 0 2px 10px color-mix(in srgb, var(--nf-accent) 45%, transparent), 0 0 0 0.5px color-mix(in srgb, var(--nf-accent) 30%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.45);
937
941
  }
938
942
 
939
943
  .buttonPrimary:hover:not([disabled]) {
940
944
  background: linear-gradient(180deg, var(--nf-accent-hover), color-mix(in srgb, var(--nf-accent-hover) 85%, #000000));
941
945
  border-color: transparent;
946
+ transform: translateY(-1px);
947
+ box-shadow: 0 12px 30px color-mix(in srgb, var(--nf-accent) 50%, transparent), 0 0 0 0.5px color-mix(in srgb, var(--nf-accent) 30%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.45);
942
948
  }
943
949
 
944
950
  .buttonDanger {
@@ -951,9 +957,9 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
951
957
  }
952
958
 
953
959
  .buttonSmall {
954
- padding: 3px 10px;
960
+ padding: 4px 10px;
955
961
  font-size: 12px;
956
- border-radius: 9px;
962
+ border-radius: 8px;
957
963
  }
958
964
 
959
965
  /* ------------------------------------------------------------ forms */
@@ -974,6 +980,7 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
974
980
  border: 1px solid var(--nf-border-strong);
975
981
  background: var(--nf-bg-inset);
976
982
  color: var(--nf-text);
983
+ color-scheme: light;
977
984
  border-radius: 12px;
978
985
  padding: 7px 11px;
979
986
  font-size: 13px;
@@ -982,6 +989,28 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
982
989
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
983
990
  }
984
991
 
992
+ /* 原生下拉浮层配色跟随主题(防白底白字) */
993
+ body[data-ds-dark-theme] .input {
994
+ color-scheme: dark;
995
+ }
996
+ .panel[data-nf-mode='dark'] .input {
997
+ color-scheme: dark;
998
+ }
999
+ .panel[data-nf-mode='light'] .input {
1000
+ color-scheme: light;
1001
+ }
1002
+ body:not([data-ds-dark-theme]) .panel[data-nf-theme='neumorph'] .input {
1003
+ color-scheme: light;
1004
+ }
1005
+ select.input {
1006
+ cursor: pointer;
1007
+ }
1008
+ .input option {
1009
+ color: var(--nf-text);
1010
+ background: var(--nf-bg-raise);
1011
+ font-size: 15px;
1012
+ }
1013
+
985
1014
  .input:focus,
986
1015
  .textarea:focus {
987
1016
  outline: none;
@@ -2113,10 +2142,10 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2113
2142
  flex: 1;
2114
2143
  min-height: 0;
2115
2144
  overflow: auto;
2116
- padding: 18px;
2145
+ padding: var(--nf-density-pad, 18px);
2117
2146
  display: flex;
2118
2147
  flex-direction: column;
2119
- gap: 16px;
2148
+ gap: var(--nf-density-gap, 16px);
2120
2149
  }
2121
2150
 
2122
2151
  .shelfHeader {
@@ -2125,9 +2154,27 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2125
2154
  gap: 10px;
2126
2155
  }
2127
2156
 
2157
+ /* 书架顶部主容器:加高玻璃卡 */
2158
+ .shelfHero {
2159
+ display: flex;
2160
+ flex-direction: column;
2161
+ gap: 14px;
2162
+ padding: var(--nf-density-card, 22px 24px);
2163
+ border: 1px solid var(--nf-glass-border);
2164
+ border-radius: 22px;
2165
+ background: var(--nf-glass-face);
2166
+ box-shadow: var(--nf-shadow), inset 0 1.5px 0 var(--nf-glass-sheen);
2167
+ }
2168
+
2169
+ .shelfHero .panelTitle {
2170
+ font-size: 22px;
2171
+ letter-spacing: 0.3px;
2172
+ }
2173
+
2128
2174
  .shelfTitleRow {
2129
2175
  display: flex;
2130
- align-items: baseline;
2176
+ align-items: center;
2177
+ justify-content: space-between;
2131
2178
  gap: 10px;
2132
2179
  flex-wrap: wrap;
2133
2180
  }
@@ -2145,10 +2192,91 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2145
2192
  max-width: 320px;
2146
2193
  }
2147
2194
 
2148
- .shelfFilters {
2195
+ .shelfStats {
2196
+ display: grid;
2197
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
2198
+ gap: 10px;
2199
+ }
2200
+
2201
+ .shelfStat {
2149
2202
  display: flex;
2150
- gap: 6px;
2151
- flex-wrap: wrap;
2203
+ align-items: baseline;
2204
+ justify-content: space-between;
2205
+ gap: 8px;
2206
+ padding: 10px 14px;
2207
+ border: 1px solid var(--nf-glass-border);
2208
+ border-radius: 14px;
2209
+ background: var(--nf-glass-face);
2210
+ box-shadow: var(--nf-shadow), inset 0 1px 0 var(--nf-glass-sheen);
2211
+ cursor: pointer;
2212
+ user-select: none;
2213
+ transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
2214
+ }
2215
+
2216
+ .shelfStat:hover {
2217
+ border-color: var(--nf-accent);
2218
+ transform: translateY(-1px);
2219
+ }
2220
+
2221
+ .shelfStatActive {
2222
+ border-color: var(--nf-accent);
2223
+ background: var(--nf-accent-soft);
2224
+ box-shadow: 0 0 0 2px var(--nf-accent-soft), var(--nf-shadow);
2225
+ }
2226
+
2227
+ .shelfStat b {
2228
+ font-size: 20px;
2229
+ font-weight: 800;
2230
+ color: var(--nf-accent);
2231
+ }
2232
+
2233
+ .shelfStat span {
2234
+ font-size: 12px;
2235
+ color: var(--nf-text-2);
2236
+ }
2237
+
2238
+ /* 筛选:胶囊分段控件 */
2239
+ .shelfFilters {
2240
+ display: inline-flex;
2241
+ gap: 2px;
2242
+ padding: 3px;
2243
+ border: 1px solid var(--nf-border);
2244
+ border-radius: 999px;
2245
+ background: var(--nf-bg-inset);
2246
+ }
2247
+
2248
+ .shelfFilter {
2249
+ border: none;
2250
+ background: transparent;
2251
+ color: var(--nf-text-2);
2252
+ font-size: 12.5px;
2253
+ font-weight: 600;
2254
+ padding: 5px 12px;
2255
+ border-radius: 999px;
2256
+ cursor: pointer;
2257
+ display: inline-flex;
2258
+ align-items: center;
2259
+ gap: 5px;
2260
+ white-space: nowrap;
2261
+ transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
2262
+ }
2263
+
2264
+ .shelfFilter:hover {
2265
+ color: var(--nf-text);
2266
+ }
2267
+
2268
+ .shelfFilterActive {
2269
+ background: linear-gradient(135deg, var(--nf-accent), var(--nf-accent-hover));
2270
+ color: var(--nf-accent-fg);
2271
+ box-shadow: 0 3px 12px color-mix(in srgb, var(--nf-accent) 40%, transparent);
2272
+ }
2273
+
2274
+ .shelfFilterCount {
2275
+ font-size: 10.5px;
2276
+ opacity: 0.8;
2277
+ background: color-mix(in srgb, currentColor 14%, transparent);
2278
+ border-radius: 999px;
2279
+ padding: 0 6px;
2152
2280
  }
2153
2281
 
2154
2282
  /* 书架空态 / 无结果引导卡 */
@@ -2180,10 +2308,171 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2180
2308
  .shelfGrid {
2181
2309
  display: grid;
2182
2310
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
2183
- gap: 14px;
2311
+ gap: var(--nf-density-gap, 14px);
2184
2312
  align-content: start;
2185
2313
  }
2186
2314
 
2315
+ /* ------------------------------------------------------------
2316
+ * 外观设置扩展:强制显示模式 + 界面密度 + 主题暗色覆盖
2317
+ * ------------------------------------------------------------ */
2318
+ .panel {
2319
+ --nf-density-card: 16px 18px;
2320
+ --nf-density-gap: 14px;
2321
+ --nf-density-pad: 16px 18px 20px;
2322
+ --nf-density-nav: 16px 12px 14px;
2323
+ }
2324
+
2325
+ .panel[data-nf-density='compact'] {
2326
+ --nf-density-card: 12px 14px;
2327
+ --nf-density-gap: 8px;
2328
+ --nf-density-pad: 12px 14px;
2329
+ --nf-density-nav: 8px 8px;
2330
+ }
2331
+
2332
+ .panel[data-nf-density='spacious'] {
2333
+ --nf-density-card: 22px 26px;
2334
+ --nf-density-gap: 18px;
2335
+ --nf-density-pad: 22px 24px;
2336
+ --nf-density-nav: 18px 14px;
2337
+ }
2338
+
2339
+ /* 强制浅色(不跟随系统时覆盖 .view 的明暗变量) */
2340
+ .panel[data-nf-mode='light'] {
2341
+ color-scheme: light;
2342
+ --nf-bg: rgba(242, 242, 247, 0.86);
2343
+ --nf-bg-raise: rgba(255, 255, 255, 0.72);
2344
+ --nf-bg-inset: rgba(118, 118, 128, 0.12);
2345
+ --nf-border: rgba(60, 60, 67, 0.16);
2346
+ --nf-border-strong: rgba(60, 60, 67, 0.32);
2347
+ --nf-text: #1c1c1e;
2348
+ --nf-text-2: #3c3c43;
2349
+ --nf-text-2-alpha: rgba(60, 60, 67, 0.68);
2350
+ --nf-text-3: #8e8e93;
2351
+ --nf-accent: #00b85c;
2352
+ --nf-accent-hover: #00a04e;
2353
+ --nf-accent-soft: rgba(0, 184, 92, 0.12);
2354
+ --nf-accent-fg: #ffffff;
2355
+ --nf-hover: rgba(0, 0, 0, 0.05);
2356
+ --nf-success: #34c759;
2357
+ --nf-error: #ff3b30;
2358
+ --nf-warn: #ff9500;
2359
+ --nf-info: #00b85c;
2360
+ --nf-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 12px 32px rgba(0, 0, 0, 0.08);
2361
+ --nf-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.07), 0 20px 48px rgba(0, 0, 0, 0.12);
2362
+ --nf-card-shadow: 0 2px 6px rgba(0, 0, 0, 0.06), 0 14px 38px rgba(0, 0, 0, 0.14);
2363
+ --nf-card-shadow-hover: 0 4px 10px rgba(0, 0, 0, 0.08), 0 22px 52px rgba(0, 0, 0, 0.20);
2364
+ --nf-glass-face: linear-gradient(180deg, #f6f7f9 0%, #f0f2f5 26%, #e9ebef 52%, #e0e3e8 78%, #d6d9df 100%);
2365
+ --nf-glass-core: radial-gradient(30% 24% at 36% 28%, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.22) 45%, rgba(255, 255, 255, 0) 78%);
2366
+ --nf-glass-sheen: rgba(255, 255, 255, 0.55);
2367
+ --nf-glass-border: rgba(255, 255, 255, 0.65);
2368
+ --nf-glass-inset: rgba(255, 255, 255, 0.55);
2369
+ --nf-glass-orb-a: rgba(0, 184, 92, 0.18);
2370
+ --nf-glass-orb-b: rgba(120, 160, 255, 0.13);
2371
+ }
2372
+
2373
+ /* 强制深色 */
2374
+ .panel[data-nf-mode='dark'] {
2375
+ color-scheme: dark;
2376
+ --nf-bg: rgba(10, 11, 13, 0.86);
2377
+ --nf-bg-raise: rgba(28, 28, 30, 0.78);
2378
+ --nf-bg-inset: rgba(118, 118, 128, 0.18);
2379
+ --nf-border: rgba(255, 255, 255, 0.12);
2380
+ --nf-border-strong: rgba(255, 255, 255, 0.28);
2381
+ --nf-text: #f2f2f7;
2382
+ --nf-text-2: #d1d1d6;
2383
+ --nf-text-2-alpha: rgba(209, 209, 214, 0.72);
2384
+ --nf-text-3: #8e8e93;
2385
+ --nf-accent: #35d07a;
2386
+ --nf-accent-hover: #5ce695;
2387
+ --nf-accent-soft: rgba(53, 208, 122, 0.2);
2388
+ --nf-accent-fg: #062b18;
2389
+ --nf-hover: rgba(255, 255, 255, 0.09);
2390
+ --nf-success: #30d158;
2391
+ --nf-error: #ff453a;
2392
+ --nf-warn: #ff9f0a;
2393
+ --nf-info: #35d07a;
2394
+ --nf-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 10px 30px rgba(0, 0, 0, 0.4);
2395
+ --nf-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4), 0 20px 48px rgba(0, 0, 0, 0.55);
2396
+ --nf-card-shadow: 0 2px 6px rgba(0, 0, 0, 0.35), 0 14px 38px rgba(0, 0, 0, 0.45);
2397
+ --nf-card-shadow-hover: 0 4px 10px rgba(0, 0, 0, 0.4), 0 22px 52px rgba(0, 0, 0, 0.55);
2398
+ --nf-glass-face: linear-gradient(180deg, rgba(112, 116, 122, 0.42) 0%, rgba(96, 100, 106, 0.42) 26%, rgba(82, 85, 90, 0.42) 52%, rgba(68, 71, 76, 0.42) 78%, rgba(56, 58, 63, 0.42) 100%);
2399
+ --nf-glass-core: radial-gradient(30% 24% at 36% 28%, rgba(255, 255, 255, 0.26) 0%, rgba(255, 255, 255, 0.10) 45%, rgba(255, 255, 255, 0) 78%);
2400
+ --nf-glass-sheen: rgba(255, 255, 255, 0.26);
2401
+ --nf-glass-border: rgba(255, 255, 255, 0.14);
2402
+ --nf-glass-inset: rgba(255, 255, 255, 0.07);
2403
+ --nf-glass-orb-a: rgba(0, 202, 82, 0.28);
2404
+ --nf-glass-orb-b: rgba(190, 200, 215, 0.10);
2405
+ }
2406
+
2407
+ /* 强制深色时:经典毛玻璃 / 新拟物 的暗色变体 */
2408
+ .panel[data-nf-mode='dark'][data-nf-theme='classic'] {
2409
+ --nf-accent: #0a84ff;
2410
+ --nf-accent-hover: #3a9bff;
2411
+ --nf-accent-soft: rgba(10, 132, 255, 0.2);
2412
+ --nf-accent-fg: #ffffff;
2413
+ --nf-info: #bf5af2;
2414
+ --nf-glass-face: var(--nf-bg-raise);
2415
+ --nf-glass-core: transparent;
2416
+ --nf-glass-sheen: rgba(255, 255, 255, 0);
2417
+ --nf-glass-border: var(--nf-border);
2418
+ --nf-glass-inset: var(--nf-bg-inset);
2419
+ --nf-glass-orb-a: transparent;
2420
+ --nf-glass-orb-b: transparent;
2421
+ --nf-glass-noise: none;
2422
+ }
2423
+
2424
+ .panel[data-nf-mode='dark'][data-nf-theme='neumorph'] {
2425
+ --nf-bg: #2b2f36;
2426
+ --nf-bg-raise: #2b2f36;
2427
+ --nf-bg-inset: #25292f;
2428
+ --nf-border: rgba(10, 12, 16, 0.5);
2429
+ --nf-border-strong: rgba(8, 10, 14, 0.6);
2430
+ --nf-text: #d6dae1;
2431
+ --nf-text-2: #a8aeb8;
2432
+ --nf-text-2-alpha: rgba(168, 174, 184, 0.7);
2433
+ --nf-text-3: #757c88;
2434
+ --nf-hover: rgba(255, 255, 255, 0.05);
2435
+ --nf-shadow: 4px 4px 10px rgba(0, 0, 0, 0.5), -4px -4px 10px rgba(96, 104, 118, 0.24);
2436
+ --nf-shadow-lg: 6px 6px 14px rgba(0, 0, 0, 0.55), -6px -6px 14px rgba(96, 104, 118, 0.26);
2437
+ --nf-card-shadow: 9px 9px 18px rgba(0, 0, 0, 0.5), -9px -9px 18px rgba(96, 104, 118, 0.24);
2438
+ --nf-card-shadow-hover: 12px 12px 24px rgba(0, 0, 0, 0.6), -12px -12px 24px rgba(96, 104, 118, 0.28);
2439
+ --nf-glass-face: #2b2f36;
2440
+ --nf-glass-core: transparent;
2441
+ --nf-glass-sheen: rgba(255, 255, 255, 0);
2442
+ --nf-glass-border: rgba(12, 14, 18, 0.45);
2443
+ --nf-glass-inset: #25292f;
2444
+ --nf-glass-orb-a: transparent;
2445
+ --nf-glass-orb-b: transparent;
2446
+ --nf-glass-noise: none;
2447
+ }
2448
+
2449
+ /* 强制浅色时:新拟物 的浅色变体(深色系统下也能用) */
2450
+ .panel[data-nf-mode='light'][data-nf-theme='neumorph'] {
2451
+ --nf-bg: #e4e9f2;
2452
+ --nf-bg-raise: #e4e9f2;
2453
+ --nf-bg-inset: #d9dfe8;
2454
+ --nf-border: rgba(140, 155, 175, 0.35);
2455
+ --nf-border-strong: rgba(120, 135, 158, 0.5);
2456
+ --nf-text: #3a4250;
2457
+ --nf-text-2: #5b6472;
2458
+ --nf-text-2-alpha: rgba(70, 80, 96, 0.7);
2459
+ --nf-text-3: #8b94a3;
2460
+ --nf-hover: rgba(90, 105, 125, 0.08);
2461
+ --nf-shadow: 4px 4px 10px rgba(163, 177, 198, 0.55), -4px -4px 10px rgba(255, 255, 255, 0.85);
2462
+ --nf-shadow-lg: 6px 6px 14px rgba(163, 177, 198, 0.6), -6px -6px 14px rgba(255, 255, 255, 0.9);
2463
+ --nf-card-shadow: 9px 9px 18px rgba(163, 177, 198, 0.55), -9px -9px 18px rgba(255, 255, 255, 0.85);
2464
+ --nf-card-shadow-hover: 12px 12px 24px rgba(163, 177, 198, 0.65), -12px -12px 24px rgba(255, 255, 255, 0.95);
2465
+ --nf-glass-face: #e4e9f2;
2466
+ --nf-glass-core: transparent;
2467
+ --nf-glass-sheen: rgba(255, 255, 255, 0);
2468
+ --nf-glass-border: rgba(163, 177, 198, 0.4);
2469
+ --nf-glass-inset: #d9dfe8;
2470
+ --nf-glass-orb-a: transparent;
2471
+ --nf-glass-orb-b: transparent;
2472
+ --nf-glass-noise: none;
2473
+ }
2474
+
2475
+
2187
2476
  .bookCard {
2188
2477
  display: flex;
2189
2478
  flex-direction: column;
@@ -2220,13 +2509,35 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2220
2509
  display: flex;
2221
2510
  align-items: center;
2222
2511
  justify-content: center;
2512
+ position: relative;
2223
2513
  }
2224
2514
 
2225
2515
  .bookCardCover img {
2226
2516
  width: 100%;
2227
2517
  height: 100%;
2228
- object-fit: contain;
2518
+ object-fit: cover;
2229
2519
  display: block;
2520
+ transition: transform 0.35s ease;
2521
+ }
2522
+
2523
+ .bookCard:hover .bookCardCover img {
2524
+ transform: scale(1.06);
2525
+ }
2526
+
2527
+ /* 封面底部渐暗(hover 出现,突出书名区) */
2528
+ .bookCardCover::after {
2529
+ content: '';
2530
+ position: absolute;
2531
+ inset: 0;
2532
+ border-radius: inherit;
2533
+ pointer-events: none;
2534
+ background: linear-gradient(180deg, transparent 55%, rgba(0, 0, 0, 0.28));
2535
+ opacity: 0;
2536
+ transition: opacity 0.25s ease;
2537
+ }
2538
+
2539
+ .bookCard:hover .bookCardCover::after {
2540
+ opacity: 1;
2230
2541
  }
2231
2542
 
2232
2543
  .bookCardCoverFallback {
@@ -2254,6 +2565,7 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2254
2565
  flex-direction: column;
2255
2566
  gap: 5px;
2256
2567
  min-width: 0;
2568
+ flex: 1;
2257
2569
  }
2258
2570
 
2259
2571
  .bookCardTitleRow {
@@ -2288,10 +2600,11 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2288
2600
  }
2289
2601
 
2290
2602
  .bookCardProgressBar {
2291
- height: 5px;
2603
+ height: 6px;
2292
2604
  border-radius: 999px;
2293
2605
  background: var(--nf-border);
2294
2606
  overflow: hidden;
2607
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12);
2295
2608
  }
2296
2609
 
2297
2610
  .bookCardProgressFill {
@@ -2310,16 +2623,25 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2310
2623
  border-style: dashed;
2311
2624
  color: var(--nf-text-2);
2312
2625
  min-height: 260px;
2626
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease, transform 0.15s ease;
2313
2627
  }
2314
2628
 
2315
2629
  .bookAddCard:hover {
2630
+ border-color: var(--nf-accent);
2631
+ background: var(--nf-accent-soft);
2316
2632
  color: var(--nf-accent);
2633
+ transform: translateY(-2px);
2317
2634
  }
2318
2635
 
2319
2636
  .bookAddIcon {
2320
2637
  font-size: 34px;
2321
2638
  line-height: 1;
2322
2639
  color: var(--nf-accent);
2640
+ transition: transform 0.15s ease;
2641
+ }
2642
+
2643
+ .bookAddCard:hover .bookAddIcon {
2644
+ transform: scale(1.12);
2323
2645
  }
2324
2646
 
2325
2647
  .bookCreateCard {
@@ -2708,7 +3030,8 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
2708
3030
  display: flex;
2709
3031
  align-items: center;
2710
3032
  gap: 6px;
2711
- margin-top: 2px;
3033
+ margin-top: auto;
3034
+ padding-top: 8px;
2712
3035
  }
2713
3036
 
2714
3037
  .bookCardActions .button {
@@ -3112,3 +3435,64 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
3112
3435
  .helpTip:focus-within .helpTipText {
3113
3436
  display: block;
3114
3437
  }
3438
+
3439
+ /* 设置页字号放大(标题明显大于正文) */
3440
+ .settingsCard {
3441
+ font-size: 15px;
3442
+ }
3443
+ .settingsCard .fieldLabel {
3444
+ font-size: 14px;
3445
+ }
3446
+ .settingsCard .input {
3447
+ font-size: 15.5px;
3448
+ }
3449
+ .settingsCard .meta {
3450
+ font-size: 13px;
3451
+ }
3452
+ .settingsCard .cardTitle {
3453
+ font-size: 20px;
3454
+ }
3455
+ .settingsCard .input {
3456
+ padding: 12px 16px;
3457
+ border-radius: 10px;
3458
+ }
3459
+ .settingsCard + .settingsCard {
3460
+ flex: 1;
3461
+ min-height: 0;
3462
+
3463
+ /* 设置页开关(滑块) */
3464
+ .switch {
3465
+ position: relative;
3466
+ width: 42px;
3467
+ height: 24px;
3468
+ border-radius: 999px;
3469
+ border: 1px solid var(--nf-border-strong);
3470
+ background: var(--nf-bg-inset);
3471
+ cursor: pointer;
3472
+ padding: 0;
3473
+ flex: none;
3474
+ transition: background 0.18s ease, border-color 0.18s ease;
3475
+ }
3476
+
3477
+ .switchKnob {
3478
+ position: absolute;
3479
+ top: 3px;
3480
+ left: 3px;
3481
+ width: 16px;
3482
+ height: 16px;
3483
+ border-radius: 50%;
3484
+ background: #fff;
3485
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
3486
+ transition: transform 0.18s ease;
3487
+ }
3488
+
3489
+ .switchOn {
3490
+ background: var(--nf-accent);
3491
+ border-color: var(--nf-accent);
3492
+ }
3493
+
3494
+ .switchOn .switchKnob {
3495
+ transform: translateX(18px);
3496
+ }
3497
+
3498
+ }
package/src/engine.ts CHANGED
@@ -2001,6 +2001,40 @@ export async function breakdownBook(
2001
2001
  }
2002
2002
  }
2003
2003
 
2004
+ /** 生图接口连通性测试(设置页模型条目用):GET {baseURL}/models,计时返回延迟。 */
2005
+ export async function testImageEndpoint(baseURL: string, apiKey: string, model?: string): Promise<{ ok: boolean; ms: number; message?: string; modelFound?: boolean }> {
2006
+ const trimmed = baseURL.trim()
2007
+ if (trimmed === '') return { ok: false, ms: 0, message: '接口地址不能为空' }
2008
+ const url = trimmed.replace(/\/+$/, '') + '/models'
2009
+ const start = Date.now()
2010
+ try {
2011
+ const res = await fetch(url, {
2012
+ method: 'GET',
2013
+ headers: { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
2014
+ })
2015
+ const ms = Date.now() - start
2016
+ if (!res.ok) {
2017
+ const text = (await res.text().catch(() => '')).slice(0, 200)
2018
+ return { ok: false, ms, message: 'HTTP ' + res.status + (text !== '' ? ':' + text : '') }
2019
+ }
2020
+ let modelFound: boolean | undefined
2021
+ if (model !== undefined && model !== '') {
2022
+ const json = await res.json().catch(() => null)
2023
+ const list = (json as { data?: unknown } | null)?.data
2024
+ if (Array.isArray(list)) {
2025
+ modelFound = list.some(m => {
2026
+ if (typeof m !== 'object' || m === null) return false
2027
+ const o = m as Record<string, unknown>
2028
+ return o.id === model || o.model === model
2029
+ })
2030
+ }
2031
+ }
2032
+ return { ok: true, ms, modelFound }
2033
+ } catch (err) {
2034
+ return { ok: false, ms: Date.now() - start, message: (err as Error).message }
2035
+ }
2036
+ }
2037
+
2004
2038
  export async function generateRoleReferenceImage(
2005
2039
  ctx: Context,
2006
2040
  config: NovelConfig,
@@ -2008,15 +2042,21 @@ export async function generateRoleReferenceImage(
2008
2042
  outputDir: string,
2009
2043
  name: string,
2010
2044
  style = '',
2045
+ modelId?: string,
2011
2046
  ): Promise<string> {
2012
2047
  const role = project.roles?.find(r => r.name === name)
2013
2048
  if (role === undefined) throw new Error(`角色 ${name} 不存在`)
2014
2049
  if (role.imagePrompt === undefined) throw new Error(`角色 ${name} 还没有形象锚点,请先生成锚点`)
2015
- if (config.imageApiEnabled !== true) throw new Error('豆包生图未启用,请在设置中开启')
2016
- const apiKey = config.imageApiKey
2017
- const model = config.imageApiModel
2050
+ if (config.imageApiEnabled !== true) throw new Error('生图未启用,请在设置中启用一个生图模型')
2051
+ const activeImage = (modelId !== undefined && modelId !== '' ? (config.imageModels ?? []).find(m => m.id === modelId) : undefined)
2052
+ ?? (config.imageModels ?? []).find(m => m.enabled)
2053
+ ?? (config.imageModels ?? [])[0]
2054
+ const apiKey = activeImage?.apiKey ?? config.imageApiKey
2055
+ const model = activeImage?.model ?? config.imageApiModel
2056
+ const baseURL = (activeImage?.baseURL ?? config.imageBaseUrl ?? '').replace(/\/+$/, '')
2057
+ if (baseURL === '') throw new Error('请先在设置中填写生图接口地址')
2018
2058
  if (apiKey === undefined || apiKey === '' || model === undefined || model === '') {
2019
- throw new Error('请先在设置中配置豆包 API Key 和生图模型')
2059
+ throw new Error('请先在设置中配置生图模型的 API Key 和模型 id')
2020
2060
  }
2021
2061
  const stylePrompt = getComicStylePrompt(style)
2022
2062
  const prompt = [
@@ -2027,7 +2067,7 @@ export async function generateRoleReferenceImage(
2027
2067
  'character reference sheet, front view, clean background, consistent design',
2028
2068
  ].filter(Boolean).join(', ')
2029
2069
 
2030
- const res = await fetch('https://ark.cn-beijing.volces.com/api/v3/images/generations', {
2070
+ const res = await fetch(baseURL + '/images/generations', {
2031
2071
  method: 'POST',
2032
2072
  headers: {
2033
2073
  'Content-Type': 'application/json',
@@ -2044,14 +2084,14 @@ export async function generateRoleReferenceImage(
2044
2084
  })
2045
2085
  if (!res.ok) {
2046
2086
  const text = await res.text()
2047
- throw new Error(`豆包生图失败(${res.status}):${text}`)
2087
+ throw new Error(`生图失败(${res.status}):${text}`)
2048
2088
  }
2049
2089
  const json = (await res.json()) as { data?: Array<{ url?: string }> }
2050
2090
  const url = json.data?.[0]?.url
2051
- if (url === undefined || url === '') throw new Error('豆包生图未返回图片 URL')
2091
+ if (url === undefined || url === '') throw new Error('生图接口未返回图片 URL')
2052
2092
 
2053
2093
  const imgRes = await fetch(url)
2054
- if (!imgRes.ok) throw new Error('下载豆包图片失败')
2094
+ if (!imgRes.ok) throw new Error('下载生图结果失败')
2055
2095
  const buf = Buffer.from(await imgRes.arrayBuffer())
2056
2096
  const contentType = imgRes.headers.get('content-type') || 'image/jpeg'
2057
2097
  const dataUrl = `data:${contentType};base64,${buf.toString('base64')}`