Files
smart-cropx-ui/src/app/(app)/ai-crop-model/data-sense-center/device-parameter/components/AddParameterDialog.tsx
peng dfc29ce01f fix: 修复系统模块TypeScript类型错误和组件功能问题
- 修复消息组件JSX.Element类型错误,改为React.ReactNode
- 完善审核历史页面类型定义和API接口调用
- 优化验证码组件,移除备用验证码逻辑避免无限循环
- 简化系统设置页面,仅保留基本设置和外观设置
- 修复用户管理页面编辑模态框数据加载和CRUD操作
- 移除废弃的作物推荐组件文件

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 17:28:11 +08:00

465 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 = String(paramForm.defaultValue).toLowerCase() === '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>
);
}