interface-section.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. import { UI_FONT_OPTIONS } from "@/lib/font-settings"
  9. interface Props {
  10. draft: SettingsDraft
  11. setDraft: DraftSetter
  12. }
  13. const UI_LANGUAGES = [
  14. { value: "en", label: "English" },
  15. { value: "zh", label: "中文" },
  16. ]
  17. const FONT_SIZE_PRESETS = [
  18. { label: "小", value: 0.9 },
  19. { label: "默认", value: 1 },
  20. { label: "大", value: 1.15 },
  21. { label: "特大", value: 1.3 },
  22. ]
  23. const VISUAL_STYLE_OPTIONS = [
  24. {
  25. value: "classic",
  26. label: "经典原版",
  27. description: "保留当前浅色、深色、深蓝护眼配色。",
  28. colors: ["#ffffff", "#171717", "#e5e5e5"],
  29. },
  30. {
  31. value: "tianqing",
  32. label: "天青釉色",
  33. description: "天青主题色、冰裂浅底、鎏金点缀。",
  34. colors: ["#3D8B9E", "#E3F3F8", "#C4A265"],
  35. },
  36. {
  37. value: "qingci",
  38. label: "青瓷墨韵",
  39. description: "青瓷主色、宣纸暖底、檀木点缀。",
  40. colors: ["#3E6360", "#EDE6D8", "#B8956A"],
  41. },
  42. {
  43. value: "yunshan",
  44. label: "云山黛色",
  45. description: "黛蓝主题色、晨雾浅底、石绿辅助。",
  46. colors: ["#35647A", "#DAEEF5", "#3A7A5C"],
  47. },
  48. {
  49. value: "cangzhu",
  50. label: "苍苍竹色",
  51. description: "竹青主题色、竹编暖纸背景、金竹辅助色。",
  52. colors: ["#3E6B58", "#EDE8D8", "#C8A96E"],
  53. },
  54. {
  55. value: "yuebai",
  56. label: "月白黛蓝",
  57. description: "黛蓝主题色、霜月浅底、琥珀点缀。",
  58. colors: ["#4A5E95", "#DDE5F0", "#C4956A"],
  59. },
  60. {
  61. value: "gumo",
  62. label: "古墨流金",
  63. description: "古金主题色、象牙浅底、玄墨层次。",
  64. colors: ["#C49A3C", "#F5F0E8", "#D4AD5A"],
  65. },
  66. ] as const
  67. const SIDEBAR_NAV_LABEL_KEYS: Record<SidebarNavItemId, string> = {
  68. wiki: "novel.nav.wiki",
  69. sources: "novel.nav.sources",
  70. graph: "novel.nav.graph",
  71. lint: "novel.nav.lint",
  72. soul: "novel.nav.soul",
  73. skillLibrary: "novel.nav.skillLibrary",
  74. bookAnalysis: "novel.nav.dismantling",
  75. reviewCenter: "novel.nav.reviewCenter",
  76. storySimulation: "novel.nav.storySimulation",
  77. search: "novel.nav.search",
  78. trash: "nav.trash",
  79. }
  80. export function InterfaceSection({ draft, setDraft }: Props) {
  81. const { t } = useTranslation()
  82. const scalePercent = Math.round(draft.uiFontSizeScale * 100)
  83. const sidebarNavConfig = normalizeSidebarNavConfig(draft.sidebarNavConfig)
  84. const hiddenSidebarNavIds = new Set(sidebarNavConfig.hidden)
  85. const handleToggleSidebarNavItem = (id: SidebarNavItemId, visible: boolean) => {
  86. const hidden = visible
  87. ? sidebarNavConfig.hidden.filter((itemId) => itemId !== id)
  88. : [...sidebarNavConfig.hidden, id]
  89. setDraft("sidebarNavConfig", normalizeSidebarNavConfig({
  90. ...sidebarNavConfig,
  91. hidden,
  92. }))
  93. }
  94. return (
  95. <div className="space-y-6">
  96. <div>
  97. <h2 className="text-xl font-semibold">{t("settings.sections.interface.title")}</h2>
  98. <p className="mt-1 text-sm text-muted-foreground">
  99. {t("settings.sections.interface.description")}
  100. </p>
  101. </div>
  102. <div className="space-y-3 rounded-lg border p-4">
  103. <div>
  104. <Label>视觉风格</Label>
  105. <p className="mt-1 text-xs text-muted-foreground">
  106. 东方美学方案不会覆盖原设计,可随时切回经典原版。
  107. </p>
  108. </div>
  109. <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
  110. {VISUAL_STYLE_OPTIONS.map((option) => {
  111. const active = draft.visualStyle === option.value
  112. return (
  113. <button
  114. key={option.value}
  115. type="button"
  116. onClick={() => setDraft("visualStyle", option.value)}
  117. className={`rounded-lg border p-3 text-left transition-colors ${
  118. active ? "border-primary bg-primary/5 ring-1 ring-primary/40" : "border-border hover:bg-accent/50"
  119. }`}
  120. >
  121. <div className="mb-3 flex gap-1.5">
  122. {option.colors.map((color) => (
  123. <span
  124. key={color}
  125. className="h-7 flex-1 rounded-md border"
  126. style={{ backgroundColor: color }}
  127. />
  128. ))}
  129. </div>
  130. <div className="text-sm font-medium">{option.label}</div>
  131. <p className="mt-1 text-xs leading-5 text-muted-foreground">{option.description}</p>
  132. </button>
  133. )
  134. })}
  135. </div>
  136. </div>
  137. <div className="space-y-2">
  138. <Label>{t("settings.sections.interface.uiLanguage")}</Label>
  139. <div className="flex flex-wrap gap-2">
  140. {UI_LANGUAGES.map((l) => {
  141. const active = draft.uiLanguage === l.value
  142. return (
  143. <button
  144. key={l.value}
  145. type="button"
  146. onClick={() => setDraft("uiLanguage", l.value)}
  147. className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${
  148. active
  149. ? "border-primary bg-primary text-primary-foreground"
  150. : "border-border hover:bg-accent"
  151. }`}
  152. >
  153. {l.label}
  154. </button>
  155. )
  156. })}
  157. </div>
  158. <p className="text-xs text-muted-foreground">
  159. {t("settings.sections.interface.uiLanguageHint")}
  160. </p>
  161. </div>
  162. <div className="space-y-3 rounded-lg border p-4">
  163. <div>
  164. <Label>{t("settings.sections.interface.sidebarNavTitle")}</Label>
  165. <p className="mt-1 text-xs text-muted-foreground">
  166. {t("settings.sections.interface.sidebarNavDescription")}
  167. </p>
  168. </div>
  169. <div className="grid gap-2">
  170. {sidebarNavConfig.order.map((id) => {
  171. const visible = !hiddenSidebarNavIds.has(id)
  172. return (
  173. <label
  174. key={id}
  175. className="flex items-center justify-between gap-3 rounded-md border px-3 py-2 text-sm transition-colors hover:bg-accent/40"
  176. >
  177. <span className="truncate">{t(SIDEBAR_NAV_LABEL_KEYS[id])}</span>
  178. <input
  179. type="checkbox"
  180. checked={visible}
  181. onChange={(e) => handleToggleSidebarNavItem(id, e.target.checked)}
  182. className="h-4 w-4 accent-primary"
  183. aria-label={t(SIDEBAR_NAV_LABEL_KEYS[id])}
  184. />
  185. </label>
  186. )
  187. })}
  188. </div>
  189. <p className="text-xs text-muted-foreground">
  190. {t("settings.sections.interface.sidebarNavOrderHint")}
  191. </p>
  192. </div>
  193. <div className="space-y-3 rounded-lg border p-4">
  194. <div>
  195. <Label>界面字体</Label>
  196. <p className="mt-1 text-xs text-muted-foreground">
  197. 默认使用本机系统字体,也可以切换为本机已安装的常见字体。
  198. </p>
  199. </div>
  200. <select
  201. value={draft.uiFontFamily}
  202. onChange={(e) => setDraft("uiFontFamily", e.target.value as SettingsDraft["uiFontFamily"])}
  203. className="w-full rounded-md border bg-background px-3 py-2 text-sm outline-none transition-colors focus:border-primary focus:ring-2 focus:ring-ring/30"
  204. aria-label="界面字体"
  205. >
  206. {UI_FONT_OPTIONS.map((option) => (
  207. <option key={option.value} value={option.value}>
  208. {option.label}
  209. </option>
  210. ))}
  211. </select>
  212. </div>
  213. <div className="space-y-3 rounded-lg border p-4">
  214. <div className="flex items-center justify-between">
  215. <Label>界面字号</Label>
  216. <span className="text-xs text-muted-foreground">{scalePercent}%</span>
  217. </div>
  218. <input
  219. type="range"
  220. min={85}
  221. max={130}
  222. step={5}
  223. value={scalePercent}
  224. onChange={(e) => setDraft("uiFontSizeScale", Number(e.target.value) / 100)}
  225. className="w-full accent-primary"
  226. aria-label="界面字号"
  227. />
  228. <div className="flex flex-wrap gap-2">
  229. {FONT_SIZE_PRESETS.map((preset) => {
  230. const active = Math.abs(draft.uiFontSizeScale - preset.value) < 0.001
  231. return (
  232. <button
  233. key={preset.value}
  234. type="button"
  235. onClick={() => setDraft("uiFontSizeScale", preset.value)}
  236. className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${
  237. active
  238. ? "border-primary bg-primary text-primary-foreground"
  239. : "border-border hover:bg-accent"
  240. }`}
  241. >
  242. {preset.label}
  243. </button>
  244. )
  245. })}
  246. </div>
  247. <p className="text-xs text-muted-foreground">
  248. 调整整个应用的字号,保存后立即生效。
  249. </p>
  250. </div>
  251. </div>
  252. )
  253. }