interface-section.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { useTranslation } from "react-i18next"
  2. import { Label } from "@/components/ui/label"
  3. import type { SettingsDraft, DraftSetter } from "../settings-types"
  4. import {
  5. normalizeSidebarNavConfig,
  6. type SidebarNavItemId,
  7. } from "@/lib/sidebar-nav-preferences"
  8. interface Props {
  9. draft: SettingsDraft
  10. setDraft: DraftSetter
  11. }
  12. const UI_LANGUAGES = [
  13. { value: "en", label: "English" },
  14. { value: "zh", label: "中文" },
  15. ]
  16. const FONT_SIZE_PRESETS = [
  17. { label: "小", value: 0.9 },
  18. { label: "默认", value: 1 },
  19. { label: "大", value: 1.15 },
  20. { label: "特大", value: 1.3 },
  21. ]
  22. const SIDEBAR_NAV_LABEL_KEYS: Record<SidebarNavItemId, string> = {
  23. wiki: "novel.nav.wiki",
  24. sources: "novel.nav.sources",
  25. graph: "novel.nav.graph",
  26. lint: "novel.nav.lint",
  27. soul: "novel.nav.soul",
  28. bookAnalysis: "novel.nav.dismantling",
  29. reviewCenter: "novel.nav.reviewCenter",
  30. storySimulation: "novel.nav.storySimulation",
  31. search: "novel.nav.search",
  32. trash: "nav.trash",
  33. }
  34. export function InterfaceSection({ draft, setDraft }: Props) {
  35. const { t } = useTranslation()
  36. const scalePercent = Math.round(draft.uiFontSizeScale * 100)
  37. const sidebarNavConfig = normalizeSidebarNavConfig(draft.sidebarNavConfig)
  38. const hiddenSidebarNavIds = new Set(sidebarNavConfig.hidden)
  39. const handleToggleSidebarNavItem = (id: SidebarNavItemId, visible: boolean) => {
  40. const hidden = visible
  41. ? sidebarNavConfig.hidden.filter((itemId) => itemId !== id)
  42. : [...sidebarNavConfig.hidden, id]
  43. setDraft("sidebarNavConfig", normalizeSidebarNavConfig({
  44. ...sidebarNavConfig,
  45. hidden,
  46. }))
  47. }
  48. return (
  49. <div className="space-y-6">
  50. <div>
  51. <h2 className="text-xl font-semibold">{t("settings.sections.interface.title")}</h2>
  52. <p className="mt-1 text-sm text-muted-foreground">
  53. {t("settings.sections.interface.description")}
  54. </p>
  55. </div>
  56. <div className="space-y-2">
  57. <Label>{t("settings.sections.interface.uiLanguage")}</Label>
  58. <div className="flex flex-wrap gap-2">
  59. {UI_LANGUAGES.map((l) => {
  60. const active = draft.uiLanguage === l.value
  61. return (
  62. <button
  63. key={l.value}
  64. type="button"
  65. onClick={() => setDraft("uiLanguage", l.value)}
  66. className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${
  67. active
  68. ? "border-primary bg-primary text-primary-foreground"
  69. : "border-border hover:bg-accent"
  70. }`}
  71. >
  72. {l.label}
  73. </button>
  74. )
  75. })}
  76. </div>
  77. <p className="text-xs text-muted-foreground">
  78. {t("settings.sections.interface.uiLanguageHint")}
  79. </p>
  80. </div>
  81. <div className="space-y-3 rounded-lg border p-4">
  82. <div>
  83. <Label>{t("settings.sections.interface.sidebarNavTitle")}</Label>
  84. <p className="mt-1 text-xs text-muted-foreground">
  85. {t("settings.sections.interface.sidebarNavDescription")}
  86. </p>
  87. </div>
  88. <div className="grid gap-2">
  89. {sidebarNavConfig.order.map((id) => {
  90. const visible = !hiddenSidebarNavIds.has(id)
  91. return (
  92. <label
  93. key={id}
  94. className="flex items-center justify-between gap-3 rounded-md border px-3 py-2 text-sm transition-colors hover:bg-accent/40"
  95. >
  96. <span className="truncate">{t(SIDEBAR_NAV_LABEL_KEYS[id])}</span>
  97. <input
  98. type="checkbox"
  99. checked={visible}
  100. onChange={(e) => handleToggleSidebarNavItem(id, e.target.checked)}
  101. className="h-4 w-4 accent-primary"
  102. aria-label={t(SIDEBAR_NAV_LABEL_KEYS[id])}
  103. />
  104. </label>
  105. )
  106. })}
  107. </div>
  108. <p className="text-xs text-muted-foreground">
  109. {t("settings.sections.interface.sidebarNavOrderHint")}
  110. </p>
  111. </div>
  112. <div className="space-y-3 rounded-lg border p-4">
  113. <div className="flex items-center justify-between">
  114. <Label>界面字号</Label>
  115. <span className="text-xs text-muted-foreground">{scalePercent}%</span>
  116. </div>
  117. <input
  118. type="range"
  119. min={85}
  120. max={130}
  121. step={5}
  122. value={scalePercent}
  123. onChange={(e) => setDraft("uiFontSizeScale", Number(e.target.value) / 100)}
  124. className="w-full accent-primary"
  125. aria-label="界面字号"
  126. />
  127. <div className="flex flex-wrap gap-2">
  128. {FONT_SIZE_PRESETS.map((preset) => {
  129. const active = Math.abs(draft.uiFontSizeScale - preset.value) < 0.001
  130. return (
  131. <button
  132. key={preset.value}
  133. type="button"
  134. onClick={() => setDraft("uiFontSizeScale", preset.value)}
  135. className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${
  136. active
  137. ? "border-primary bg-primary text-primary-foreground"
  138. : "border-border hover:bg-accent"
  139. }`}
  140. >
  141. {preset.label}
  142. </button>
  143. )
  144. })}
  145. </div>
  146. <p className="text-xs text-muted-foreground">
  147. 调整整个应用的字号,保存后立即生效。
  148. </p>
  149. </div>
  150. </div>
  151. )
  152. }