mosaicmap/components/admin/field-renderer.tsx
2025-08-15 22:31:51 +08:00

274 lines
9.7 KiB
TypeScript

"use client";
import React from "react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { FieldConfig } from "@/types/admin-panel";
interface FieldRendererProps {
field: FieldConfig;
value?: any;
error?: string;
disabled?: boolean;
onChange: (value: any) => void;
onBlur?: () => void;
className?: string;
form_field?: any;
}
export function FieldRenderer({
field,
value,
error,
disabled = false,
onChange,
onBlur,
className,
form_field
}: FieldRendererProps) {
const isDisabled = disabled || field.disabled;
const isReadOnly = field.readOnly;
// Use custom render function if provided
if (field.render) {
return (
<div className={className}>
{field.render(field, value, onChange)}
</div>
);
}
const renderField = () => {
switch (field.type) {
case "input":
case "email":
case "url":
case "tel":
return (
<Input
{...form_field}
/>
);
case "password":
return (
<Input
type="password"
{...form_field}
/>
);
case "number":
return (
<Input
type="number"
{...form_field}
/>
);
case "textarea":
return (
<Textarea
rows={field.rows || 3}
{...form_field}
/>
);
case "select":
return (
<Select
value={form_field.value?.toString() || ""}
onValueChange={(newValue) => {
// Convert back to number if the original value was a number
const option = field.options?.find(opt => opt.value?.toString() === newValue);
form_field.onChange(option ? option.value : newValue)
onChange(option ? option.value : newValue);
}}
disabled={isDisabled}
>
<SelectTrigger className={cn(
error && "border-destructive focus:ring-destructive",
className
)}>
<SelectValue placeholder={field.placeholder || "请选择..."} />
</SelectTrigger>
<SelectContent>
{field.options?.map((option) => (
<SelectItem
key={option.value}
value={option.value.toString()}
disabled={option.disabled}
>
<div>
<div>{option.label}</div>
{option.description && (
<div className="text-xs text-muted-foreground">
{option.description}
</div>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
case "switch":
return (
<Switch
id={field.id}
checked={form_field.value}
onCheckedChange={(c) => {
form_field.onChange(c)
onChange(c);
}}
disabled={isDisabled}
className={cn(
error && "border-destructive",
className
)}
/>
);
case "checkbox":
return (
<Checkbox
id={field.id}
checked={form_field.value}
onCheckedChange={(c) => {
form_field.onChange(c)
onChange(c);
}}
disabled={isDisabled}
className={cn(
error && "border-destructive",
className
)}
/>
);
case "radio":
return (
<RadioGroup
value={value?.toString() || ""}
onValueChange={(newValue) => {
const option = field.options?.find(opt => opt.value?.toString() === newValue);
onChange(option ? option.value : newValue);
}}
disabled={isDisabled}
className={className}
>
{field.options?.map((option) => (
<div key={option.value} className="flex items-center space-x-2">
<RadioGroupItem
value={option.value.toString()}
id={`${field.id}-${option.value}`}
disabled={option.disabled}
/>
<Label
htmlFor={`${field.id}-${option.value}`}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{option.label}
</Label>
{option.description && (
<span className="text-xs text-muted-foreground ml-2">
{option.description}
</span>
)}
</div>
))}
</RadioGroup>
);
case "slider":
case "range":
const sliderValue = Array.isArray(value) ? value : [value || field.min || 0];
return (
<div className="space-y-3">
<Slider
value={sliderValue}
onValueChange={(newValue) => onChange(newValue[0])}
min={field.min || 0}
max={field.max || 100}
step={field.step || 1}
disabled={isDisabled}
className={cn(
error && "accent-destructive",
className
)}
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{field.min || 0}</span>
<span className="font-medium">{sliderValue[0]}</span>
<span>{field.max || 100}</span>
</div>
</div>
);
case "date":
case "time":
case "datetime-local":
return (
<Input
type={field.type}
{...form_field}
/>
);
case "color":
return (
<div className="flex items-center space-x-3">
<Input
type="color"
{...form_field}
className="w-12 h-10 p-1 rounded border cursor-pointer"
/>
<Input
type="text"
{...form_field}
placeholder="#000000"
className={cn(
"flex-1",
error && "border-destructive focus-visible:ring-destructive"
)}
/>
</div>
);
case "file":
return (
<Input
type="file"
{...form_field}
/>
);
default:
return (
<Input
type="text"
{...form_field}
/>
);
}
};
return (
<div className="space-y-2">
{renderField()}
{error && (
<p className="text-sm font-medium text-destructive">{error}</p>
)}
</div>
);
}