interface-section.tsx 9.4 KB

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