diff --git a/crop-x/src/app/(app)/ai-crop-model/data-sense-center/device-parameter/components/AddParameterDialog.tsx b/crop-x/src/app/(app)/ai-crop-model/data-sense-center/device-parameter/components/AddParameterDialog.tsx new file mode 100644 index 0000000..7cf085b --- /dev/null +++ b/crop-x/src/app/(app)/ai-crop-model/data-sense-center/device-parameter/components/AddParameterDialog.tsx @@ -0,0 +1,465 @@ +/** + * filekorolheader: 添加/编辑参数对话框 - 参数模板配置组件 + * 功能:新增参数、编辑参数信息、表单验证、不同类型参数配置 + * 路径:/ai-crop-model/data-sense-center/device-parameter/components + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式 + */ + +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { ParameterDefinition, DeviceType, DeviceParameterAction } from './deviceParameterReducer'; +import { Save, X, Plus } from 'lucide-react'; +import { toast } from 'sonner'; + +interface AddParameterDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + editingParam: ParameterDefinition | null; + selectedType: DeviceType | null; + dispatch: React.Dispatch; +} + +interface ParamForm { + key: string; + label: string; + type: 'string' | 'number' | 'boolean' | 'select'; + required: boolean; + defaultValue: string; + unit: string; + min: string; + max: string; + description: string; + options: { label: string; value: string }[]; +} + +export function AddParameterDialog({ open, onOpenChange, editingParam, selectedType, dispatch }: AddParameterDialogProps) { + const [paramForm, setParamForm] = useState({ + key: '', + label: '', + type: 'string', + required: false, + defaultValue: '', + unit: '', + min: '', + max: '', + description: '', + options: [] + }); + + const [optionLabel, setOptionLabel] = useState(''); + const [optionValue, setOptionValue] = useState(''); + const [errors, setErrors] = useState>>({}); + + useEffect(() => { + if (editingParam) { + setParamForm({ + key: editingParam.key, + label: editingParam.label, + type: editingParam.type, + required: editingParam.required || false, + defaultValue: editingParam.defaultValue?.toString() || '', + unit: editingParam.unit || '', + min: editingParam.min?.toString() || '', + max: editingParam.max?.toString() || '', + description: editingParam.description || '', + options: editingParam.options || [] + }); + } else { + setParamForm({ + key: '', + label: '', + type: 'string', + required: false, + defaultValue: '', + unit: '', + min: '', + max: '', + description: '', + options: [] + }); + } + setErrors({}); + setOptionLabel(''); + setOptionValue(''); + }, [editingParam, open]); + + const validateForm = (): boolean => { + const newErrors: Partial> = {}; + + if (!paramForm.key.trim()) { + newErrors.key = '请输入参数标识'; + } + + if (!paramForm.label.trim()) { + newErrors.label = '请输入参数名称'; + } + + // 检查参数标识是否重复(编辑时除外) + if (!editingParam || editingParam.key !== paramForm.key) { + const exists = selectedType?.parameterDefinitions?.some(p => p.key === paramForm.key.trim()); + if (exists) { + newErrors.key = '参数标识已存在'; + } + } + + // 验证数字类型的范围 + if (paramForm.type === 'number') { + if (paramForm.min && paramForm.max) { + const min = parseFloat(paramForm.min); + const max = parseFloat(paramForm.max); + if (min >= max) { + newErrors.max = '最大值必须大于最小值'; + } + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSaveParam = () => { + if (!selectedType) return; + + if (!validateForm()) { + return; + } + + try { + // 构建新参数 + const newParam: ParameterDefinition = { + key: paramForm.key.trim(), + label: paramForm.label.trim(), + type: paramForm.type, + required: paramForm.required, + description: paramForm.description.trim() + }; + + // 根据类型设置默认值和属性 + if (paramForm.type === 'number') { + newParam.defaultValue = paramForm.defaultValue ? parseFloat(paramForm.defaultValue) : 0; + if (paramForm.unit) newParam.unit = paramForm.unit; + if (paramForm.min) newParam.min = parseFloat(paramForm.min); + if (paramForm.max) newParam.max = parseFloat(paramForm.max); + } else if (paramForm.type === 'boolean') { + newParam.defaultValue = paramForm.defaultValue === 'true' || paramForm.defaultValue === true; + } else if (paramForm.type === 'select') { + newParam.options = paramForm.options; + newParam.defaultValue = paramForm.defaultValue || (paramForm.options[0]?.value || ''); + } else { + newParam.defaultValue = paramForm.defaultValue; + } + + if (editingParam) { + dispatch({ type: 'UPDATE_PARAMETER', payload: newParam }); + } else { + dispatch({ type: 'ADD_PARAMETER', payload: newParam }); + } + + onOpenChange(false); + } catch (error) { + console.error('保存失败:', error); + toast.error('保存失败,请重试'); + } + }; + + const handleAddOption = () => { + if (!optionLabel.trim() || !optionValue.trim()) { + toast.error('请填写选项标签和值'); + return; + } + + const newOption = { label: optionLabel.trim(), value: optionValue.trim() }; + setParamForm({ + ...paramForm, + options: [...paramForm.options, newOption] + }); + setOptionLabel(''); + setOptionValue(''); + }; + + const handleRemoveOption = (index: number) => { + setParamForm({ + ...paramForm, + options: paramForm.options.filter((_, i) => i !== index) + }); + }; + + const handleInputChange = (field: keyof ParamForm, value: any) => { + setParamForm(prev => ({ ...prev, [field]: value })); + // 清除该字段的错误 + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: undefined })); + } + }; + + return ( + + + + + {editingParam ? '编辑参数' : '新增参数'} + + + 为 {selectedType?.name} 配置参数定义 + + + +
+ {/* 基本信息 */} +
+
+ + handleInputChange('key', e.target.value)} + placeholder="例如:temperature" + disabled={!!editingParam} + className={errors.key ? 'border-red-500' : ''} + /> + {errors.key && ( +

{errors.key}

+ )} +

+ 英文标识,用于数据传输 +

+
+ +
+ + handleInputChange('label', e.target.value)} + placeholder="例如:温度" + className={errors.label ? 'border-red-500' : ''} + /> + {errors.label && ( +

{errors.label}

+ )} +
+
+ +
+
+ + +
+ +
+ +
+ handleInputChange('required', checked)} + /> + +
+
+
+ + {/* 根据类型显示不同字段 */} + {paramForm.type === 'number' && ( +
+
+ + handleInputChange('unit', e.target.value)} + placeholder="例如:°C" + /> +
+
+ + handleInputChange('min', e.target.value)} + placeholder="最小值" + /> +
+
+ + handleInputChange('max', e.target.value)} + placeholder="最大值" + className={errors.max ? 'border-red-500' : ''} + /> + {errors.max && ( +

{errors.max}

+ )} +
+
+ )} + + {/* 选择类型的选项配置 */} + {paramForm.type === 'select' && ( +
+ +
+
+ setOptionLabel(e.target.value)} + placeholder="选项标签(显示文本)" + className="flex-1" + /> + setOptionValue(e.target.value)} + placeholder="选项值" + className="flex-1" + /> + +
+ + {paramForm.options.length > 0 && ( +
+

已添加的选项:

+ {paramForm.options.map((option, index) => ( +
+ + {option.label} ({option.value}) + + +
+ ))} +
+ )} +
+
+ )} + + {/* 默认值 */} +
+ + {paramForm.type === 'boolean' ? ( + + ) : paramForm.type === 'select' && paramForm.options.length > 0 ? ( + + ) : ( + handleInputChange('defaultValue', e.target.value)} + placeholder="参数默认值" + /> + )} +
+ + {/* 描述 */} +
+ +