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 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 17:16:02 +08:00
1 parent 04cffcd1f1
commit 639fdeacae
13 files changed
+254 -111

No files matched your search

+18 -2
View File
@@ -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<GlobalSettings>("/settings", { signal }),
queryFn: ({ signal }) => apiData<GlobalSettings>("/settings", { signal }).then(withSettingsDefaults),
});
}
/**
* 后端比前端旧时(滚动升级、回滚)响应里会缺新加的 key;按 key 用默认值补齐,
* 避免设置页读 `settings.<新key>.x` 直接崩掉。只补缺的,不覆盖已有值。
*/
export function withSettingsDefaults(s: GlobalSettings): GlobalSettings {
const out: Record<string, unknown> = { ...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<GlobalSettings>("/settings", { method: "PATCH", body: patch }),
mutationFn: (patch: GlobalSettingsUpdate) =>
apiData<GlobalSettings>("/settings", { method: "PATCH", body: patch }).then(withSettingsDefaults),
onMutate: async (patch) => {
await qc.cancelQueries({ queryKey: qk.settings });
const prev = qc.getQueryData<GlobalSettings>(qk.settings);