import { type GlobalSettings, GlobalSettings as GlobalSettingsSchema, type GlobalSettingsUpdate, mergeSettings, } from "@umt/contracts"; import { LoaderCircle } from "lucide-react"; import { type ReactNode, useState } from "react"; import { toast } from "sonner"; import { useSettings, useUpdateSettings } from "~/api/settings.ts"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { zodFieldErrors } from "~/components/console/form-field.tsx"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { FieldControl, type FieldSpec, type FormValues } from "./resource-section.tsx"; import { SettingsBlock } from "./settings-block.tsx"; // 全局设置的一节:一组字段,独立保存(PATCH 对应 key); // 校验 = 合并现值后用 contracts GlobalSettings 整体校验(含"恢复水位 < 硬阈值"等交叉规则)。 export type CardSpec = { /** 锚点 id(分区内唯一) */ anchor: string; title: string; description?: ReactNode; fields: FieldSpec[]; fromSettings: (s: GlobalSettings) => FormValues; toPatch: (v: FormValues, s: GlobalSettings) => GlobalSettingsUpdate; extra?: (s: GlobalSettings) => ReactNode; }; export function SettingsCard({ spec, settings, section, }: { spec: CardSpec; settings: GlobalSettings; section: string; }) { const update = useUpdateSettings(); const [values, setValues] = useState(() => spec.fromSettings(settings)); const [errors, setErrors] = useState>({}); const initial = spec.fromSettings(settings); const dirty = Object.keys(values).some((k) => values[k] !== initial[k]); const save = async (e: React.FormEvent) => { e.preventDefault(); const patch = spec.toPatch(values, settings); const parsed = GlobalSettingsSchema.safeParse(mergeSettings(settings, patch)); if (!parsed.success) { const fe = zodFieldErrors(parsed.error.issues); // 交叉校验(refine)落在分组上:挂到本节同组的第一个字段 for (const [k, m] of Object.entries(fe)) { if (!spec.fields.some((f) => f.name === k)) fe[spec.fields.find((f) => k.startsWith(f.name.split(".")[0] ?? ""))?.name ?? k] ??= m; } setErrors(fe); return; } setErrors({}); try { const next = await update.mutateAsync(patch); setValues(spec.fromSettings(next)); toast.success(`${spec.title}已保存`); } catch (err) { toast.error(errorMessage(err, "保存失败")); } }; return (
{spec.fields .filter((f) => !f.visible || f.visible(values)) .map((f) => ( setValues((s) => ({ ...s, [f.name]: v }))} /> ))}
{spec.extra?.(settings)}
{dirty ? ( ) : null}
); } /** 读取全局设置后再渲染(加载 / 出错占位) */ export function SettingsGate({ children }: { children: (s: GlobalSettings) => ReactNode }) { const q = useSettings(); if (q.isPending) return ; if (q.isError) return q.refetch()} />; return <>{children(q.data)}; } /** 数字 → 表单字符串(null 为空) */ export const n = (v: number | null | undefined) => (v === null || v === undefined ? "" : String(v));