From dd2835bb94e143241c1f7e4ebc08969fbd0d747e Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Sat, 6 Jun 2026 22:55:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E8=AE=BE=E7=BD=AE=E9=A1=B5?= =?UTF-8?q?=E6=94=B9=E9=80=A0=E4=B8=BA=20Form=20+=20Radio.Group=20+=20Swit?= =?UTF-8?q?ch,=E7=B4=A7=E5=87=91=E6=A8=A1=E5=BC=8F=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/web/features/settings/index.tsx | 57 ++++++++++++++----- .../features/settings/SettingsPage.test.tsx | 50 +++++++++++++--- 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/web/features/settings/index.tsx b/src/web/features/settings/index.tsx index ea3a9a6..a84434e 100644 --- a/src/web/features/settings/index.tsx +++ b/src/web/features/settings/index.tsx @@ -1,5 +1,6 @@ -import { Card, Segmented } from "antd"; +import { App as AntApp, Card, Form, Radio, Switch } from "antd"; +import type { ThemePreference } from "../../../shared/api"; import { useSettings } from "../../shared/hooks/use-settings"; import { parseThemePreference, useThemePreference } from "../../shared/hooks/use-theme-preference"; @@ -9,30 +10,60 @@ const THEME_OPTIONS = [ { label: "黑暗", value: "dark" }, ] as const; +const SAVE_MESSAGE_KEY = "settings-save"; + export function SettingsPage() { - const { preference, setPreference } = useThemePreference(); + const { message } = AntApp.useApp(); + const { compact, preference, setCompact, setPreference } = useThemePreference(); const { isUpdating, updateSettings } = useSettings(); - const handleThemeChange = (value: string) => { - const theme = parseThemePreference(value); + const handleThemeChange = (value: ThemePreference) => { + message.open({ content: "保存中...", duration: 0, key: SAVE_MESSAGE_KEY, type: "loading" }); updateSettings( - { theme }, + { theme: value }, { + onError: () => { + void message.open({ content: "保存失败", duration: 2, key: SAVE_MESSAGE_KEY, type: "error" }); + }, onSuccess: () => { - setPreference(theme); + setPreference(value); + void message.open({ content: "已保存", duration: 1.5, key: SAVE_MESSAGE_KEY, type: "success" }); + }, + }, + ); + }; + + const handleCompactChange = (checked: boolean) => { + message.open({ content: "保存中...", duration: 0, key: SAVE_MESSAGE_KEY, type: "loading" }); + updateSettings( + { compact: checked }, + { + onError: () => { + void message.open({ content: "保存失败", duration: 2, key: SAVE_MESSAGE_KEY, type: "error" }); + }, + onSuccess: () => { + setCompact(checked); + void message.open({ content: "已保存", duration: 1.5, key: SAVE_MESSAGE_KEY, type: "success" }); }, }, ); }; return ( - - handleThemeChange(value)} - options={THEME_OPTIONS.map((option) => ({ label: option.label, value: option.value }))} - value={preference} - /> + +
+ + handleThemeChange(parseThemePreference(e.target.value))} + options={THEME_OPTIONS.map((o) => ({ label: o.label, value: o.value }))} + value={preference} + /> + + + + +
); } diff --git a/tests/web/features/settings/SettingsPage.test.tsx b/tests/web/features/settings/SettingsPage.test.tsx index 51af860..54a760c 100644 --- a/tests/web/features/settings/SettingsPage.test.tsx +++ b/tests/web/features/settings/SettingsPage.test.tsx @@ -5,8 +5,8 @@ import { createElement } from "react"; import { SettingsPage } from "../../../../src/web/features/settings/index"; import { installFetchMock, jsonResponse, renderWithProviders } from "../../test-utils"; -function mockSettingsResponse(theme = "system"): Response { - return jsonResponse({ theme }); +function mockSettingsResponse(theme = "system", compact = false): Response { + return jsonResponse({ compact, theme }); } describe("SettingsPage", () => { @@ -21,7 +21,7 @@ describe("SettingsPage", () => { expect(screen.getByText("主题")).not.toBeNull(); }); - test("渲染主题 Segmented 选项", () => { + test("渲染主题模式 Radio.Group 选项", () => { installFetchMock((call) => { if (call.url.includes("/api/settings")) return mockSettingsResponse(); return jsonResponse({}); @@ -34,7 +34,41 @@ describe("SettingsPage", () => { expect(screen.getByText("黑暗")).not.toBeNull(); }); - test("API 加载中时不显示保存状态", () => { + test("渲染紧凑模式标签和开关", () => { + installFetchMock((call) => { + if (call.url.includes("/api/settings")) return mockSettingsResponse(); + return jsonResponse({}); + }); + + renderWithProviders(createElement(SettingsPage)); + + expect(screen.getByText("紧凑模式")).not.toBeNull(); + }); + + test("渲染水平表单结构", () => { + installFetchMock((call) => { + if (call.url.includes("/api/settings")) return mockSettingsResponse(); + return jsonResponse({}); + }); + + renderWithProviders(createElement(SettingsPage)); + + const form = document.querySelector(".ant-form"); + expect(form).not.toBeNull(); + }); + + test("不再使用 Segmented", () => { + installFetchMock((call) => { + if (call.url.includes("/api/settings")) return mockSettingsResponse(); + return jsonResponse({}); + }); + + renderWithProviders(createElement(SettingsPage)); + + expect(document.querySelector(".ant-segmented")).toBeNull(); + }); + + test("不显示保存状态文本(已迁移到 toast)", () => { installFetchMock((call) => { if (call.url.includes("/api/settings")) return mockSettingsResponse(); return jsonResponse({}); @@ -45,17 +79,17 @@ describe("SettingsPage", () => { expect(screen.queryByText("保存中...")).toBeNull(); }); - test("GET /api/settings 获取已保存主题", async () => { + test("GET /api/settings 获取已保存主题和紧凑设置", async () => { installFetchMock((call) => { - if (call.url.includes("/api/settings")) return mockSettingsResponse("dark"); + if (call.url.includes("/api/settings")) return mockSettingsResponse("dark", true); return jsonResponse({}); }); renderWithProviders(createElement(SettingsPage)); await waitFor(() => { - const segmented = document.querySelector(".ant-segmented"); - expect(segmented).not.toBeNull(); + const radioGroup = document.querySelector(".ant-radio-group"); + expect(radioGroup).not.toBeNull(); }); }); });