- 修复消息组件JSX.Element类型错误,改为React.ReactNode - 完善审核历史页面类型定义和API接口调用 - 优化验证码组件,移除备用验证码逻辑避免无限循环 - 简化系统设置页面,仅保留基本设置和外观设置 - 修复用户管理页面编辑模态框数据加载和CRUD操作 - 移除废弃的作物推荐组件文件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
465 lines
16 KiB
TypeScript
465 lines
16 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
} |