Files
UHD_Media_Tool/apps/web/app/api/settings.ts
T
rootandClaude Opus 5.5 639fdeacae 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]>
2026-09-29 17:16:02 +08:00

62 lines
2.2 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
DEFAULT_SETTINGS,
type GlobalSettings,
type GlobalSettingsUpdate,
mergeSettings,
type WebSearchTestResult,
} from "@umt/contracts";
import { apiData } from "~/lib/api.ts";
import { qk } from "~/lib/query-keys.ts";
export function useSettings() {
return useQuery({
queryKey: qk.settings,
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 }).then(withSettingsDefaults),
onMutate: async (patch) => {
await qc.cancelQueries({ queryKey: qk.settings });
const prev = qc.getQueryData<GlobalSettings>(qk.settings);
if (prev) qc.setQueryData(qk.settings, mergeSettings(prev, patch));
return { prev };
},
onError: (_e, _p, ctx) => {
if (ctx?.prev) qc.setQueryData(qk.settings, ctx.prev);
},
onSuccess: (data) => {
qc.setQueryData(qk.settings, data);
qc.invalidateQueries({ queryKey: qk.dashboard });
},
});
}
/** POST /websearch/test:用已保存的搜索服务 Key 实际查一次 */
export function useTestWebSearch() {
return useMutation({
mutationFn: (body: { query?: string } = {}) =>
apiData<WebSearchTestResult>("/websearch/test", { method: "POST", body }),
});
}