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:
1 parent
04cffcd1f1
commit
639fdeacae
13 files changed
+254
-111
No files matched your search
@@ -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<Row extends { id: string }>({
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Button size="sm" onClick={() => setEditing("new")}>
|
||||
<Button size="sm" className="ml-auto" onClick={() => setEditing("new")}>
|
||||
<Plus />
|
||||
添加{config.noun}
|
||||
</Button>
|
||||
@@ -275,11 +276,54 @@ export function FieldControl({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let control: ReactNode;
|
||||
return (
|
||||
<FormField
|
||||
label={spec.label}
|
||||
htmlFor={id}
|
||||
error={error}
|
||||
hint={hint}
|
||||
required={spec.required}
|
||||
className={wide ? "sm:col-span-2" : undefined}
|
||||
>
|
||||
<FieldInput id={id} spec={spec} value={value} error={error} onChange={onChange} disabled={disabled} />
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
/** 字段的控件本体(不含标签与说明);开关也在这里 */
|
||||
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 (
|
||||
<Switch
|
||||
id={id}
|
||||
checked={value === true}
|
||||
onCheckedChange={onChange}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
aria-invalid={!!error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (spec.kind === "select") {
|
||||
control = (
|
||||
return (
|
||||
<Select value={String(value ?? "")} onValueChange={onChange} disabled={disabled}>
|
||||
<SelectTrigger id={id} className="w-full" aria-invalid={!!error}>
|
||||
<SelectTrigger id={id} className={cn("w-full", className)} aria-invalid={!!error}>
|
||||
<SelectValue placeholder={spec.placeholder ?? "选择"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
@@ -291,8 +335,9 @@ export function FieldControl({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else if (spec.kind === "textarea") {
|
||||
control = (
|
||||
}
|
||||
if (spec.kind === "textarea") {
|
||||
return (
|
||||
<Textarea
|
||||
id={id}
|
||||
value={String(value ?? "")}
|
||||
@@ -301,41 +346,25 @@ export function FieldControl({
|
||||
disabled={disabled}
|
||||
aria-invalid={!!error}
|
||||
rows={3}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
control = (
|
||||
<Input
|
||||
id={id}
|
||||
type={
|
||||
spec.kind === "password"
|
||||
? "password"
|
||||
: spec.kind === "number"
|
||||
? "number"
|
||||
: spec.kind === "url"
|
||||
? "url"
|
||||
: "text"
|
||||
}
|
||||
autoComplete={spec.kind === "password" ? "new-password" : "off"}
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={spec.placeholder}
|
||||
disabled={disabled}
|
||||
aria-invalid={!!error}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormField
|
||||
label={spec.label}
|
||||
htmlFor={id}
|
||||
error={error}
|
||||
hint={hint}
|
||||
required={spec.required}
|
||||
className={wide ? "sm:col-span-2" : undefined}
|
||||
>
|
||||
{control}
|
||||
</FormField>
|
||||
<Input
|
||||
id={id}
|
||||
type={
|
||||
spec.kind === "password" ? "password" : spec.kind === "number" ? "number" : spec.kind === "url" ? "url" : "text"
|
||||
}
|
||||
inputMode={spec.kind === "number" ? "decimal" : undefined}
|
||||
autoComplete={spec.kind === "password" ? "new-password" : "off"}
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={spec.placeholder}
|
||||
disabled={disabled}
|
||||
aria-invalid={!!error}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user