From 639fdeacae40b478266a7ac4072f53c0810f3466 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 17:16:02 +0800 Subject: [PATCH] fix(web): settings page layout pass (FE-7) - Section header (h2 + description) and section blocks as h3; unified block padding and title size - Global settings forms use label-left / control-right rows, stacked below md; control width fixed by type (number short, text/select medium, wide/textarea full) - Per-block SaveBar pinned at the block end with dirty hint; naming template saves moved from the header to the bar - Console shell uses overflow-x-clip so the sticky header and sticky section nav actually stick; anchor jumps re-align while async content loads and land below the header - Section content uses a single minmax(0,1fr) column so wide tables scroll inside their card instead of widening the page - useSettings fills missing keys from DEFAULT_SETTINGS so an older backend no longer crashes the metadata section Co-Authored-By: Claude Opus 5.5 --- apps/web/app/api/settings.ts | 20 +++- apps/web/app/components/settings/account.tsx | 2 +- .../app/components/settings/agent-tools.tsx | 2 +- apps/web/app/components/settings/agent.tsx | 2 +- .../app/components/settings/automation.tsx | 2 +- apps/web/app/components/settings/library.tsx | 2 +- apps/web/app/components/settings/naming.tsx | 54 ++++----- .../components/settings/resource-section.tsx | 103 +++++++++++------- .../components/settings/settings-block.tsx | 38 +++++-- .../app/components/settings/settings-card.tsx | 87 +++++++++++++-- apps/web/app/components/settings/sites.tsx | 2 +- apps/web/app/routes/console/layout.tsx | 4 +- apps/web/app/routes/console/settings.tsx | 47 +++++--- 13 files changed, 254 insertions(+), 111 deletions(-) diff --git a/apps/web/app/api/settings.ts b/apps/web/app/api/settings.ts index a6c2f380..57a7fd75 100644 --- a/apps/web/app/api/settings.ts +++ b/apps/web/app/api/settings.ts @@ -1,5 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { + DEFAULT_SETTINGS, type GlobalSettings, type GlobalSettingsUpdate, mergeSettings, @@ -11,15 +12,30 @@ import { qk } from "~/lib/query-keys.ts"; export function useSettings() { return useQuery({ queryKey: qk.settings, - queryFn: ({ signal }) => apiData("/settings", { signal }), + queryFn: ({ signal }) => apiData("/settings", { signal }).then(withSettingsDefaults), }); } +/** + * 后端比前端旧时(滚动升级、回滚)响应里会缺新加的 key;按 key 用默认值补齐, + * 避免设置页读 `settings.<新key>.x` 直接崩掉。只补缺的,不覆盖已有值。 + */ +export function withSettingsDefaults(s: GlobalSettings): GlobalSettings { + const out: Record = { ...s }; + for (const [k, def] of Object.entries(DEFAULT_SETTINGS)) { + const cur = out[k]; + if (cur === undefined || cur === null) out[k] = def; + else if (typeof def === "object" && !Array.isArray(def) && typeof cur === "object") out[k] = { ...def, ...cur }; + } + return out as GlobalSettings; +} + /** PATCH /settings,乐观更新(失败回滚) */ export function useUpdateSettings() { const qc = useQueryClient(); return useMutation({ - mutationFn: (patch: GlobalSettingsUpdate) => apiData("/settings", { method: "PATCH", body: patch }), + mutationFn: (patch: GlobalSettingsUpdate) => + apiData("/settings", { method: "PATCH", body: patch }).then(withSettingsDefaults), onMutate: async (patch) => { await qc.cancelQueries({ queryKey: qk.settings }); const prev = qc.getQueryData(qk.settings); diff --git a/apps/web/app/components/settings/account.tsx b/apps/web/app/components/settings/account.tsx index 4ea7ebc2..4978a6fb 100644 --- a/apps/web/app/components/settings/account.tsx +++ b/apps/web/app/components/settings/account.tsx @@ -189,7 +189,7 @@ export function AccountSection() { const session = useSession(); const cookie = !!session.data?.expiresAt; return ( -
+
diff --git a/apps/web/app/components/settings/agent-tools.tsx b/apps/web/app/components/settings/agent-tools.tsx index 0bb47ce3..dab92a83 100644 --- a/apps/web/app/components/settings/agent-tools.tsx +++ b/apps/web/app/components/settings/agent-tools.tsx @@ -70,7 +70,7 @@ const percent = (n: number, d: number) => (d > 0 ? `${Math.round((n / d) * 100)} export function AgentToolsBlock() { return ( -
+
diff --git a/apps/web/app/components/settings/agent.tsx b/apps/web/app/components/settings/agent.tsx index 99e093aa..68d8231c 100644 --- a/apps/web/app/components/settings/agent.tsx +++ b/apps/web/app/components/settings/agent.tsx @@ -211,7 +211,7 @@ const ADVANCED_CARD: CardSpec = { export function AgentSection() { return ( -
+
{(s) => ( diff --git a/apps/web/app/components/settings/automation.tsx b/apps/web/app/components/settings/automation.tsx index fdf1315f..3b4a98d0 100644 --- a/apps/web/app/components/settings/automation.tsx +++ b/apps/web/app/components/settings/automation.tsx @@ -173,7 +173,7 @@ function Switches() { export function AutomationSection() { return ( -
+
{(s) => ( diff --git a/apps/web/app/components/settings/library.tsx b/apps/web/app/components/settings/library.tsx index 851bc811..bf351fb4 100644 --- a/apps/web/app/components/settings/library.tsx +++ b/apps/web/app/components/settings/library.tsx @@ -41,7 +41,7 @@ const ORGANIZE_CARD: CardSpec = { export function LibrarySection() { return ( -
+
diff --git a/apps/web/app/components/settings/naming.tsx b/apps/web/app/components/settings/naming.tsx index 1cafc4e7..17eb0a77 100644 --- a/apps/web/app/components/settings/naming.tsx +++ b/apps/web/app/components/settings/naming.tsx @@ -12,7 +12,7 @@ import { Button } from "~/components/ui/button.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; -import { SettingsBlock } from "./settings-block.tsx"; +import { SaveBar, SettingsBlock } from "./settings-block.tsx"; // 设置 → 媒体库与目录 → 命名模板:全局模板 + 库根覆盖(作品覆盖在作品详情)。 @@ -61,19 +61,7 @@ function GlobalNaming() { }; return ( - - - {save.isPending ? "保存中…" : "保存"} - - } - > + {!q.data || !draft ? ( q.isError ? ( q.refetch()} /> @@ -110,6 +98,12 @@ function GlobalNaming() { aria-label="已有目录的作品固定文件名" /> + + +
)} @@ -199,22 +193,6 @@ function RootNamingEditor({ root }: { root: LibraryRoot }) { {NAMING_KIND_LABEL[k]}:{saved[k] ? "库根覆盖" : "沿用全局"} ))} -
- - -
+ + + +
); } diff --git a/apps/web/app/components/settings/resource-section.tsx b/apps/web/app/components/settings/resource-section.tsx index ee804e0e..71cdfa7f 100644 --- a/apps/web/app/components/settings/resource-section.tsx +++ b/apps/web/app/components/settings/resource-section.tsx @@ -15,6 +15,7 @@ import { Switch } from "~/components/ui/switch.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; import { revealFirstError } from "~/lib/form-errors.ts"; +import { cn } from "~/lib/utils.ts"; // 设置页里"实例类"资源的通用段落:列表 + 新建/编辑抽屉 + 删除 + 行内动作。 // 表单用 contracts 的 Create/Update schema 做 zod 校验;凭证字段只写,编辑时留空表示不变。 @@ -119,7 +120,7 @@ export function ResourceSection({ ) : ( )} - @@ -275,11 +276,54 @@ export function FieldControl({
); } - let control: ReactNode; + return ( + + + + ); +} + +/** 字段的控件本体(不含标签与说明);开关也在这里 */ +export function FieldInput({ + id, + spec, + value, + error, + onChange, + disabled, + className, +}: { + id: string; + spec: FieldSpec; + value: FieldValue | undefined; + error?: string; + onChange: (v: FieldValue) => void; + disabled?: boolean; + className?: string; +}) { + if (spec.kind === "switch") { + return ( + + ); + } if (spec.kind === "select") { - control = ( + return ( ); - } else if (spec.kind === "textarea") { - control = ( + } + if (spec.kind === "textarea") { + return (