274 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
} |