子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View File

@@ -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<DeviceParameterAction>;
}
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<ParamForm>({
key: '',
label: '',
type: 'string',
required: false,
defaultValue: '',
unit: '',
min: '',
max: '',
description: '',
options: []
});
const [optionLabel, setOptionLabel] = useState('');
const [optionValue, setOptionValue] = useState('');
const [errors, setErrors] = useState<Partial<Record<keyof ParamForm, string>>>({});
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<Record<keyof ParamForm, string>> = {};
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
{editingParam ? '编辑参数' : '新增参数'}
</DialogTitle>
<DialogDescription>
{selectedType?.name}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 基本信息 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="key" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="key"
value={paramForm.key}
onChange={(e) => handleInputChange('key', e.target.value)}
placeholder="例如temperature"
disabled={!!editingParam}
className={errors.key ? 'border-red-500' : ''}
/>
{errors.key && (
<p className="text-sm text-red-500">{errors.key}</p>
)}
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="label" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="label"
value={paramForm.label}
onChange={(e) => handleInputChange('label', e.target.value)}
placeholder="例如:温度"
className={errors.label ? 'border-red-500' : ''}
/>
{errors.label && (
<p className="text-sm text-red-500">{errors.label}</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="type" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Select
value={paramForm.type}
onValueChange={(value: any) => handleInputChange('type', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="boolean"></SelectItem>
<SelectItem value="select"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="required" className="text-sm font-medium">
</Label>
<div className="flex items-center space-x-2 h-10">
<Switch
id="required"
checked={paramForm.required}
onCheckedChange={(checked) => handleInputChange('required', checked)}
/>
<Label htmlFor="required" className="cursor-pointer text-sm">
{paramForm.required ? '必填' : '选填'}
</Label>
</div>
</div>
</div>
{/* 根据类型显示不同字段 */}
{paramForm.type === 'number' && (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="unit" className="text-sm font-medium">
</Label>
<Input
id="unit"
value={paramForm.unit}
onChange={(e) => handleInputChange('unit', e.target.value)}
placeholder="例如°C"
/>
</div>
<div className="space-y-2">
<Label htmlFor="min" className="text-sm font-medium">
</Label>
<Input
id="min"
type="number"
value={paramForm.min}
onChange={(e) => handleInputChange('min', e.target.value)}
placeholder="最小值"
/>
</div>
<div className="space-y-2">
<Label htmlFor="max" className="text-sm font-medium">
</Label>
<Input
id="max"
type="number"
value={paramForm.max}
onChange={(e) => handleInputChange('max', e.target.value)}
placeholder="最大值"
className={errors.max ? 'border-red-500' : ''}
/>
{errors.max && (
<p className="text-sm text-red-500">{errors.max}</p>
)}
</div>
</div>
)}
{/* 选择类型的选项配置 */}
{paramForm.type === 'select' && (
<div className="space-y-2">
<Label className="text-sm font-medium"></Label>
<div className="border rounded-lg p-4 space-y-3 bg-muted/30">
<div className="flex gap-2">
<Input
value={optionLabel}
onChange={(e) => setOptionLabel(e.target.value)}
placeholder="选项标签(显示文本)"
className="flex-1"
/>
<Input
value={optionValue}
onChange={(e) => setOptionValue(e.target.value)}
placeholder="选项值"
className="flex-1"
/>
<Button onClick={handleAddOption} size="sm">
<Plus className="w-4 h-4" />
</Button>
</div>
{paramForm.options.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground"></p>
{paramForm.options.map((option, index) => (
<div key={index} className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm">
{option.label} <span className="text-muted-foreground">({option.value})</span>
</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveOption(index)}
className="h-6 w-6 p-0"
>
<X className="w-3 h-3" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* 默认值 */}
<div className="space-y-2">
<Label htmlFor="defaultValue" className="text-sm font-medium">
</Label>
{paramForm.type === 'boolean' ? (
<Select
value={paramForm.defaultValue?.toString() || 'false'}
onValueChange={(value) => handleInputChange('defaultValue', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
) : paramForm.type === 'select' && paramForm.options.length > 0 ? (
<Select
value={paramForm.defaultValue}
onValueChange={(value) => handleInputChange('defaultValue', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{paramForm.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id="defaultValue"
type={paramForm.type === 'number' ? 'number' : 'text'}
value={paramForm.defaultValue}
onChange={(e) => handleInputChange('defaultValue', e.target.value)}
placeholder="参数默认值"
/>
)}
</div>
{/* 描述 */}
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
</Label>
<Textarea
id="description"
value={paramForm.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="参数说明..."
rows={3}
/>
</div>
</div>
<DialogFooter className="flex justify-between sm:justify-between">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="px-6"
>
<X className="w-4 h-4 mr-2" />
</Button>
<Button
type="button"
onClick={handleSaveParam}
className="px-6"
>
<Save className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}