- 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]>
62 lines
2.2 KiB
TypeScript
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 }),
|
|
});
|
|
}
|