生产管理系统 - 设备类型管理、设备参数管理
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 删除参数确认对话框 - 参数删除确认组件
|
||||||
|
* 功能:确认删除参数、防止误操作、影响提示
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||||
|
import { DeviceParameterAction } from './deviceParameterReducer';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DeleteParameterConfirmDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
pendingDeleteParam: string | null;
|
||||||
|
dispatch: React.Dispatch<DeviceParameterAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteParameterConfirmDialog({ open, onOpenChange, pendingDeleteParam, dispatch }: DeleteParameterConfirmDialogProps) {
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (pendingDeleteParam) {
|
||||||
|
dispatch({ type: 'DELETE_PARAMETER', payload: pendingDeleteParam });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
|
||||||
|
<AlertTriangle className="w-5 h-5" />
|
||||||
|
确认删除参数
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-2">
|
||||||
|
<p>
|
||||||
|
确定要删除此参数吗?此操作不可撤销。
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
删除后,该参数的配置和相关数据将被永久删除。
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => onOpenChange(false)}>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={confirmDelete}
|
||||||
|
className="bg-destructive hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
确认删除
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备参数统计组件 - 参数模板统计展示组件
|
||||||
|
* 功能:显示设备类型统计数据、参数配置情况、参数类型分布
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { DeviceParameterState } from './deviceParameterReducer';
|
||||||
|
import { BarChart3, Settings, Database, TrendingUp } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DeviceParameterStatsProps {
|
||||||
|
state: DeviceParameterState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceParameterStats({ state }: DeviceParameterStatsProps) {
|
||||||
|
const { deviceTypes, selectedType } = state;
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const totalParams = deviceTypes.reduce((sum, type) => sum + (type.parameterDefinitions?.length || 0), 0);
|
||||||
|
const typesWithParams = deviceTypes.filter(type => (type.parameterDefinitions?.length || 0) > 0).length;
|
||||||
|
const currentTypeParams = selectedType?.parameterDefinitions?.length || 0;
|
||||||
|
|
||||||
|
// 参数类型统计
|
||||||
|
const paramTypeStats = deviceTypes.reduce((acc, type) => {
|
||||||
|
type.parameterDefinitions?.forEach(param => {
|
||||||
|
acc[param.type] = (acc[param.type] || 0) + 1;
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
const getParamTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'number': '数字',
|
||||||
|
'string': '文本',
|
||||||
|
'boolean': '布尔',
|
||||||
|
'select': '选择'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* 设备类型总数 */}
|
||||||
|
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400 font-medium">
|
||||||
|
设备类型总数
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-green-700 dark:text-green-300 mt-1">
|
||||||
|
{deviceTypes.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-green-500 dark:text-green-400">
|
||||||
|
<Settings className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 已配置模板 */}
|
||||||
|
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-orange-600 dark:text-orange-400 font-medium">
|
||||||
|
已配置模板
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-orange-700 dark:text-orange-300 mt-1">
|
||||||
|
{typesWithParams} / {deviceTypes.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-orange-600 dark:text-orange-400 mt-1">
|
||||||
|
{deviceTypes.length > 0 ? Math.round((typesWithParams / deviceTypes.length) * 100) : 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-orange-500 dark:text-orange-400">
|
||||||
|
<TrendingUp className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 当前类型参数 */}
|
||||||
|
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-purple-600 dark:text-purple-400 font-medium">
|
||||||
|
当前类型参数
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-purple-700 dark:text-purple-300 mt-1">
|
||||||
|
{currentTypeParams}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-purple-600 dark:text-purple-400 mt-1">
|
||||||
|
{selectedType?.name || '未选择'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-purple-500 dark:text-purple-400">
|
||||||
|
<BarChart3 className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备参数表格组件 - 参数模板列表展示组件
|
||||||
|
* 功能:参数列表展示、操作按钮、参数类型显示、取值范围显示
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Eye, Edit, Trash2 } from 'lucide-react';
|
||||||
|
import { DeviceParameterState } from './deviceParameterReducer';
|
||||||
|
|
||||||
|
interface DeviceParameterTableProps {
|
||||||
|
state: DeviceParameterState;
|
||||||
|
onEdit: (param: any) => void;
|
||||||
|
onView: (param: any) => void;
|
||||||
|
onDelete: (paramKey: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceParameterTable({ state, onEdit, onView, onDelete }: DeviceParameterTableProps) {
|
||||||
|
const { selectedType } = state;
|
||||||
|
|
||||||
|
const getParamTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'number': '数字',
|
||||||
|
'string': '文本',
|
||||||
|
'boolean': '布尔',
|
||||||
|
'select': '选择'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParamTypeColor = (type: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'number': 'border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300',
|
||||||
|
'string': 'border-green-200 dark:border-green-800 text-green-700 dark:text-green-300',
|
||||||
|
'boolean': 'border-purple-200 dark:border-purple-800 text-purple-700 dark:text-purple-300',
|
||||||
|
'select': 'border-orange-200 dark:border-orange-800 text-orange-700 dark:text-orange-300'
|
||||||
|
};
|
||||||
|
return colors[type] || 'border-gray-200 dark:border-gray-800 text-gray-700 dark:text-gray-300';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
{selectedType ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-primary">{selectedType.name} - 参数配置</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{selectedType.description || '暂无描述'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="font-light">
|
||||||
|
{selectedType.parameterDefinitions?.length || 0} 个参数
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedType.parameterDefinitions && selectedType.parameterDefinitions.length > 0 ? (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[150px]">参数标识</TableHead>
|
||||||
|
<TableHead className="w-[150px]">参数名称</TableHead>
|
||||||
|
<TableHead className="w-[100px]">类型</TableHead>
|
||||||
|
<TableHead className="w-[80px]">必填</TableHead>
|
||||||
|
<TableHead className="w-[120px]">默认值</TableHead>
|
||||||
|
<TableHead className="w-[150px]">单位/范围</TableHead>
|
||||||
|
<TableHead className="min-w-[200px]">说明</TableHead>
|
||||||
|
<TableHead className="text-right w-[120px]">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{selectedType.parameterDefinitions.map((param) => (
|
||||||
|
<TableRow key={param.key} className="hover:bg-muted/30">
|
||||||
|
<TableCell className="font-mono text-sm font-medium">
|
||||||
|
{param.key}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="font-medium">{param.label}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`font-light ${getParamTypeColor(param.type)}`}
|
||||||
|
>
|
||||||
|
{getParamTypeLabel(param.type)}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{param.required ? (
|
||||||
|
<Badge className="bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 font-light">
|
||||||
|
必填
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-sm font-light">选填</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{param.type === 'boolean' ? (
|
||||||
|
<Badge
|
||||||
|
variant={param.defaultValue ? 'default' : 'secondary'}
|
||||||
|
className="font-light"
|
||||||
|
>
|
||||||
|
{param.defaultValue ? '是' : '否'}
|
||||||
|
</Badge>
|
||||||
|
) : param.type === 'select' && param.options ? (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="font-medium">
|
||||||
|
{param.options.find(opt => opt.value === param.defaultValue)?.label || param.defaultValue}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
({param.options.length} 个选项)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{param.defaultValue?.toString() || '-'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm">
|
||||||
|
{param.unit && (
|
||||||
|
<span className="font-medium">{param.unit}</span>
|
||||||
|
)}
|
||||||
|
{param.type === 'number' && (param.min !== undefined || param.max !== undefined) && (
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{param.min !== undefined && param.max !== undefined
|
||||||
|
? `(${param.min}~${param.max})`
|
||||||
|
: param.min !== undefined
|
||||||
|
? `(≥${param.min})`
|
||||||
|
: param.max !== undefined
|
||||||
|
? `(≤${param.max})`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{param.type === 'select' && param.options && (
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{param.options.length} 个选项
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<div
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
title={param.description}
|
||||||
|
>
|
||||||
|
{param.description || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onView(param)}
|
||||||
|
title="查看详情"
|
||||||
|
className="p-1 h-auto hover:bg-blue-50 dark:hover:bg-blue-950"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onEdit(param)}
|
||||||
|
title="编辑"
|
||||||
|
className="p-1 h-auto hover:bg-amber-50 dark:hover:bg-amber-950"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDelete(param.key)}
|
||||||
|
title="删除"
|
||||||
|
className="p-1 h-auto hover:bg-red-50 dark:hover:bg-red-950"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-lg font-medium text-muted-foreground">
|
||||||
|
暂无参数定义
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
该设备类型尚未配置参数模板,点击"新增参数"开始配置
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-lg font-medium text-muted-foreground">
|
||||||
|
请选择设备类型
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
从左侧列表中选择一个设备类型以查看和管理其参数模板
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备类型选择器组件 - 设备类型选择列表组件
|
||||||
|
* 功能:设备类型列表展示、选中状态管理、参数数量显示
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { DeviceParameterState, DeviceParameterAction } from './deviceParameterReducer';
|
||||||
|
|
||||||
|
interface DeviceTypeSelectorProps {
|
||||||
|
state: DeviceParameterState;
|
||||||
|
dispatch: React.Dispatch<DeviceParameterAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceTypeSelector({ state, dispatch }: DeviceTypeSelectorProps) {
|
||||||
|
const { deviceTypes, selectedType } = state;
|
||||||
|
|
||||||
|
const handleTypeSelect = (typeId: string) => {
|
||||||
|
const type = deviceTypes.find(t => t.id === typeId);
|
||||||
|
if (type) {
|
||||||
|
dispatch({ type: 'SET_SELECTED_TYPE', payload: type });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<h3 className="mb-4 text-primary">设备类型列表</h3>
|
||||||
|
{deviceTypes.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-8">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>暂无设备类型</div>
|
||||||
|
<div className="text-sm">请先在"设备类型管理"中添加设备类型</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{deviceTypes.map(type => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
onClick={() => handleTypeSelect(type.id)}
|
||||||
|
className={`p-3 rounded-lg cursor-pointer transition-all ${
|
||||||
|
selectedType?.id === type.id
|
||||||
|
? 'bg-primary-muted border-2 border-primary'
|
||||||
|
: 'bg-muted hover:bg-accent border-2 border-transparent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="truncate font-medium">{type.name}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{type.manufacturer || '未知品牌'} {type.model ? `· ${type.model}` : ''}
|
||||||
|
</p>
|
||||||
|
{type.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||||
|
{type.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="ml-2 font-light">
|
||||||
|
{type.parameterDefinitions?.length || 0}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 查看参数对话框 - 参数详情展示组件
|
||||||
|
* 功能:展示参数详细信息、参数配置、选项列表
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { ParameterDefinition, DeviceType } from './deviceParameterReducer';
|
||||||
|
import { Eye, Settings, Hash } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ViewParameterDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
viewingParam: ParameterDefinition | null;
|
||||||
|
selectedType: DeviceType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewParameterDialog({ open, onOpenChange, viewingParam, selectedType }: ViewParameterDialogProps) {
|
||||||
|
const getParamTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'number': '数字',
|
||||||
|
'string': '文本',
|
||||||
|
'boolean': '布尔',
|
||||||
|
'select': '选择'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParamTypeColor = (type: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'number': 'border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950',
|
||||||
|
'string': 'border-green-200 dark:border-green-800 text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950',
|
||||||
|
'boolean': 'border-purple-200 dark:border-purple-800 text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950',
|
||||||
|
'select': 'border-orange-200 dark:border-orange-800 text-orange-700 dark:text-orange-300 bg-orange-50 dark:bg-orange-950'
|
||||||
|
};
|
||||||
|
return colors[type] || 'border-gray-200 dark:border-gray-800 text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-950';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-xl">
|
||||||
|
<Eye className="w-5 h-5" />
|
||||||
|
参数详情
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
查看参数的详细配置信息
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{viewingParam && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 参数基本信息 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Hash className="w-5 h-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-medium">基本信息</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">参数标识</div>
|
||||||
|
<div className="font-mono text-sm font-medium bg-muted p-2 rounded">
|
||||||
|
{viewingParam.key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">参数名称</div>
|
||||||
|
<div className="font-medium text-base">{viewingParam.label}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">数据类型</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`font-light ${getParamTypeColor(viewingParam.type)}`}
|
||||||
|
>
|
||||||
|
{getParamTypeLabel(viewingParam.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">是否必填</div>
|
||||||
|
{viewingParam.required ? (
|
||||||
|
<Badge className="bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 font-light">
|
||||||
|
必填
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-sm font-light">选填</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">参数描述</div>
|
||||||
|
<div className="text-sm bg-muted/30 p-3 rounded">
|
||||||
|
{viewingParam.description || '暂无描述'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 参数配置 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Settings className="w-5 h-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-medium">参数配置</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">默认值</div>
|
||||||
|
<div className="bg-muted/30 p-3 rounded">
|
||||||
|
{viewingParam.type === 'boolean' ? (
|
||||||
|
<Badge
|
||||||
|
variant={viewingParam.defaultValue ? 'default' : 'secondary'}
|
||||||
|
className="font-light"
|
||||||
|
>
|
||||||
|
{viewingParam.defaultValue ? '是' : '否'}
|
||||||
|
</Badge>
|
||||||
|
) : viewingParam.type === 'select' && viewingParam.options ? (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">
|
||||||
|
{viewingParam.options.find(opt => opt.value === viewingParam.defaultValue)?.label || viewingParam.defaultValue}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
值: {viewingParam.defaultValue}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium">
|
||||||
|
{viewingParam.defaultValue?.toString() || '-'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{viewingParam.unit && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">单位</div>
|
||||||
|
<div className="bg-muted/30 p-3 rounded font-medium">
|
||||||
|
{viewingParam.unit}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewingParam.type === 'number' && (viewingParam.min !== undefined || viewingParam.max !== undefined) && (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">取值范围</div>
|
||||||
|
<div className="bg-muted/30 p-3 rounded font-medium">
|
||||||
|
{viewingParam.min !== undefined && viewingParam.max !== undefined
|
||||||
|
? `${viewingParam.min} ~ ${viewingParam.max}`
|
||||||
|
: viewingParam.min !== undefined
|
||||||
|
? `≥ ${viewingParam.min}`
|
||||||
|
: viewingParam.max !== undefined
|
||||||
|
? `≤ ${viewingParam.max}`
|
||||||
|
: '-'}
|
||||||
|
{viewingParam.unit && ` ${viewingParam.unit}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 选择类型选项 */}
|
||||||
|
{viewingParam.type === 'select' && viewingParam.options && (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Settings className="w-5 h-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-medium">选项列表</h3>
|
||||||
|
<Badge variant="outline" className="font-light">
|
||||||
|
{viewingParam.options.length} 个选项
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{viewingParam.options.map((option, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 bg-muted/30 rounded">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{option.label}</span>
|
||||||
|
<span className="text-muted-foreground text-sm ml-2">
|
||||||
|
({option.value})
|
||||||
|
</span>
|
||||||
|
{viewingParam.defaultValue === option.value && (
|
||||||
|
<Badge variant="default" className="ml-2 text-xs">
|
||||||
|
默认
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 设备信息 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="text-sm text-muted-foreground mb-2">所属设备类型</div>
|
||||||
|
<div className="font-medium">{selectedType?.name}</div>
|
||||||
|
{selectedType?.manufacturer && (
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
|
{selectedType.manufacturer} {selectedType.model ? `· ${selectedType.model}` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={() => onOpenChange(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备参数管理状态管理器 - 参数模板配置中心
|
||||||
|
* 功能:设备类型数据管理、参数模板配置、对话框状态控制、操作处理
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,useReducer状态管理模式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 参数定义接口
|
||||||
|
export interface ParameterDefinition {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'select';
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
options?: { label: string; value: string }[];
|
||||||
|
unit?: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备类型接口
|
||||||
|
export interface DeviceType {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
model?: string;
|
||||||
|
description?: string;
|
||||||
|
parameterDefinitions: ParameterDefinition[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态接口
|
||||||
|
export interface DeviceParameterState {
|
||||||
|
deviceTypes: DeviceType[];
|
||||||
|
selectedType: DeviceType | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
showAddParamDialog: boolean;
|
||||||
|
showViewParamDialog: boolean;
|
||||||
|
showDeleteConfirm: boolean;
|
||||||
|
|
||||||
|
// 编辑/查看数据
|
||||||
|
editingParam: ParameterDefinition | null;
|
||||||
|
viewingParam: ParameterDefinition | null;
|
||||||
|
pendingDeleteParam: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action类型定义
|
||||||
|
export type DeviceParameterAction =
|
||||||
|
| { type: 'LOAD_DATA' }
|
||||||
|
| { type: 'SET_DEVICE_TYPES'; payload: DeviceType[] }
|
||||||
|
| { type: 'SET_SELECTED_TYPE'; payload: DeviceType | null }
|
||||||
|
| { type: 'SHOW_ADD_PARAM_DIALOG' }
|
||||||
|
| { type: 'SET_ADD_PARAM_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SHOW_EDIT_PARAM_DIALOG'; payload: ParameterDefinition }
|
||||||
|
| { type: 'SHOW_VIEW_PARAM_DIALOG'; payload: ParameterDefinition }
|
||||||
|
| { type: 'SET_VIEW_PARAM_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SHOW_DELETE_CONFIRM'; payload: string }
|
||||||
|
| { type: 'SET_DELETE_CONFIRM'; payload: boolean }
|
||||||
|
| { type: 'ADD_PARAMETER'; payload: ParameterDefinition }
|
||||||
|
| { type: 'UPDATE_PARAMETER'; payload: ParameterDefinition }
|
||||||
|
| { type: 'DELETE_PARAMETER'; payload: string }
|
||||||
|
| { type: 'CLEAR_EDITING_PARAM' };
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
export const initialState: DeviceParameterState = {
|
||||||
|
deviceTypes: [],
|
||||||
|
selectedType: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
showAddParamDialog: false,
|
||||||
|
showViewParamDialog: false,
|
||||||
|
showDeleteConfirm: false,
|
||||||
|
editingParam: null,
|
||||||
|
viewingParam: null,
|
||||||
|
pendingDeleteParam: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reducer函数
|
||||||
|
export function deviceParameterReducer(state: DeviceParameterState, action: DeviceParameterAction): DeviceParameterState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'LOAD_DATA':
|
||||||
|
return loadData(state);
|
||||||
|
|
||||||
|
case 'SET_DEVICE_TYPES':
|
||||||
|
return { ...state, deviceTypes: action.payload };
|
||||||
|
|
||||||
|
case 'SET_SELECTED_TYPE':
|
||||||
|
return { ...state, selectedType: action.payload };
|
||||||
|
|
||||||
|
case 'SHOW_ADD_PARAM_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAddParamDialog: true,
|
||||||
|
editingParam: null
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_ADD_PARAM_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAddParamDialog: action.payload,
|
||||||
|
editingParam: action.payload ? null : state.editingParam
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_EDIT_PARAM_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAddParamDialog: true,
|
||||||
|
editingParam: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_VIEW_PARAM_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showViewParamDialog: true,
|
||||||
|
viewingParam: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_VIEW_PARAM_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showViewParamDialog: action.payload,
|
||||||
|
viewingParam: action.payload ? null : state.viewingParam
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_DELETE_CONFIRM':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showDeleteConfirm: true,
|
||||||
|
pendingDeleteParam: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_DELETE_CONFIRM':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showDeleteConfirm: action.payload,
|
||||||
|
pendingDeleteParam: action.payload ? null : state.pendingDeleteParam
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'ADD_PARAMETER':
|
||||||
|
return addParameter(state, action.payload);
|
||||||
|
|
||||||
|
case 'UPDATE_PARAMETER':
|
||||||
|
return updateParameter(state, action.payload);
|
||||||
|
|
||||||
|
case 'DELETE_PARAMETER':
|
||||||
|
return deleteParameter(state, action.payload);
|
||||||
|
|
||||||
|
case 'CLEAR_EDITING_PARAM':
|
||||||
|
return { ...state, editingParam: null };
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
function loadData(state: DeviceParameterState): DeviceParameterState {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem('smart_agriculture_ai_device_types');
|
||||||
|
if (data) {
|
||||||
|
const types = JSON.parse(data);
|
||||||
|
const normalizedTypes = types.map((type: any) => ({
|
||||||
|
...type,
|
||||||
|
parameterDefinitions: type.parameterDefinitions || []
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: normalizedTypes,
|
||||||
|
selectedType: normalizedTypes.length > 0 ? normalizedTypes[0] : null
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 创建模拟数据
|
||||||
|
const mockTypes: DeviceType[] = [
|
||||||
|
{
|
||||||
|
id: 'device-type-1',
|
||||||
|
name: '土壤传感器',
|
||||||
|
manufacturer: '施耐德',
|
||||||
|
model: 'SOIL-100',
|
||||||
|
description: '高精度土壤温湿度、养分监测传感器',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'measurementInterval',
|
||||||
|
label: '测量间隔',
|
||||||
|
type: 'number',
|
||||||
|
unit: '分钟',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 30,
|
||||||
|
min: 5,
|
||||||
|
max: 120,
|
||||||
|
description: '数据测量时间间隔'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'uploadMode',
|
||||||
|
label: '上传模式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '实时上传', value: 'realtime' },
|
||||||
|
{ label: '定时上传', value: 'scheduled' }
|
||||||
|
],
|
||||||
|
defaultValue: 'scheduled',
|
||||||
|
description: '数据上传方式'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'depth',
|
||||||
|
label: '测量深度',
|
||||||
|
type: 'number',
|
||||||
|
unit: 'cm',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 10,
|
||||||
|
min: 5,
|
||||||
|
max: 100,
|
||||||
|
description: '传感器在土壤中的测量深度'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device-type-2',
|
||||||
|
name: '气象站',
|
||||||
|
manufacturer: '华为',
|
||||||
|
model: 'WEATHER-200',
|
||||||
|
description: '多参数气象监测站,支持温度、湿度、风速、降雨量等监测',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'sampleRate',
|
||||||
|
label: '采样频率',
|
||||||
|
type: 'number',
|
||||||
|
unit: '次/小时',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 12,
|
||||||
|
min: 1,
|
||||||
|
max: 60,
|
||||||
|
description: '每小时采集次数'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'windSpeedUnit',
|
||||||
|
label: '风速单位',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '米/秒', value: 'm/s' },
|
||||||
|
{ label: '公里/小时', value: 'km/h' }
|
||||||
|
],
|
||||||
|
defaultValue: 'm/s'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'autoCalibration',
|
||||||
|
label: '自动校准',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: true,
|
||||||
|
description: '是否启用设备自动校准功能'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device-type-3',
|
||||||
|
name: '虫情监测仪',
|
||||||
|
manufacturer: '托普云农',
|
||||||
|
model: 'PEST-300',
|
||||||
|
description: 'AI智能虫情监测仪,自动拍照识别害虫种类和数量',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'photoInterval',
|
||||||
|
label: '拍照间隔',
|
||||||
|
type: 'number',
|
||||||
|
unit: '小时',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 2,
|
||||||
|
min: 1,
|
||||||
|
max: 24,
|
||||||
|
description: '自动拍照时间间隔'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'aiRecognition',
|
||||||
|
label: 'AI识别',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: true,
|
||||||
|
description: '是否启用AI自动识别'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'detectionSensitivity',
|
||||||
|
label: '检测灵敏度',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '高灵敏度', value: 'high' },
|
||||||
|
{ label: '中灵敏度', value: 'medium' },
|
||||||
|
{ label: '低灵敏度', value: 'low' }
|
||||||
|
],
|
||||||
|
defaultValue: 'medium',
|
||||||
|
description: '害虫检测的灵敏度设置'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device-type-4',
|
||||||
|
name: '水质监测仪',
|
||||||
|
manufacturer: '海康威视',
|
||||||
|
model: 'WATER-400',
|
||||||
|
description: '多参数水质在线监测仪,支持pH、溶解氧、浊度等监测',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'measurementCycle',
|
||||||
|
label: '测量周期',
|
||||||
|
type: 'number',
|
||||||
|
unit: '分钟',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 15,
|
||||||
|
min: 5,
|
||||||
|
max: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'alarmEnabled',
|
||||||
|
label: '启用预警',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'phThreshold',
|
||||||
|
label: 'pH阈值',
|
||||||
|
type: 'number',
|
||||||
|
unit: 'pH',
|
||||||
|
defaultValue: 7.0,
|
||||||
|
min: 6.0,
|
||||||
|
max: 8.5,
|
||||||
|
description: 'pH值异常预警阈值'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device-type-5',
|
||||||
|
name: '灌溉控制器',
|
||||||
|
manufacturer: '大疆',
|
||||||
|
model: 'IRRI-500',
|
||||||
|
description: '智能灌溉控制器,支持远程控制和定时灌溉',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'controlMode',
|
||||||
|
label: '控制模式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '手动控制', value: 'manual' },
|
||||||
|
{ label: '自动控制', value: 'auto' },
|
||||||
|
{ label: '定时控制', value: 'scheduled' }
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'auto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'flowRateLimit',
|
||||||
|
label: '流量限制',
|
||||||
|
type: 'number',
|
||||||
|
unit: 'L/min',
|
||||||
|
defaultValue: 100,
|
||||||
|
min: 10,
|
||||||
|
max: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'moistureThreshold',
|
||||||
|
label: '湿度阈值',
|
||||||
|
type: 'number',
|
||||||
|
unit: '%',
|
||||||
|
defaultValue: 30,
|
||||||
|
min: 10,
|
||||||
|
max: 80,
|
||||||
|
description: '土壤湿度触发灌溉的阈值'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(mockTypes));
|
||||||
|
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(mockTypes));
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: mockTypes,
|
||||||
|
selectedType: mockTypes[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载数据失败:', error);
|
||||||
|
toast.error('加载数据失败');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加参数
|
||||||
|
function addParameter(state: DeviceParameterState, newParam: ParameterDefinition): DeviceParameterState {
|
||||||
|
try {
|
||||||
|
if (!state.selectedType) return state;
|
||||||
|
|
||||||
|
const updatedParams = [...state.selectedType.parameterDefinitions, newParam];
|
||||||
|
const updatedType: DeviceType = {
|
||||||
|
...state.selectedType,
|
||||||
|
parameterDefinitions: updatedParams,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTypes = state.deviceTypes.map(t =>
|
||||||
|
t.id === state.selectedType?.id ? updatedType : t
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(updatedTypes));
|
||||||
|
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(updatedTypes));
|
||||||
|
toast.success('参数添加成功');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: updatedTypes,
|
||||||
|
selectedType: updatedType,
|
||||||
|
showAddParamDialog: false,
|
||||||
|
editingParam: null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加失败:', error);
|
||||||
|
toast.error('添加失败');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新参数
|
||||||
|
function updateParameter(state: DeviceParameterState, updatedParam: ParameterDefinition): DeviceParameterState {
|
||||||
|
try {
|
||||||
|
if (!state.selectedType) return state;
|
||||||
|
|
||||||
|
const updatedParams = state.selectedType.parameterDefinitions.map(p =>
|
||||||
|
p.key === updatedParam.key ? updatedParam : p
|
||||||
|
);
|
||||||
|
const updatedType: DeviceType = {
|
||||||
|
...state.selectedType,
|
||||||
|
parameterDefinitions: updatedParams,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTypes = state.deviceTypes.map(t =>
|
||||||
|
t.id === state.selectedType?.id ? updatedType : t
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(updatedTypes));
|
||||||
|
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(updatedTypes));
|
||||||
|
toast.success('参数更新成功');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: updatedTypes,
|
||||||
|
selectedType: updatedType,
|
||||||
|
showAddParamDialog: false,
|
||||||
|
editingParam: null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新失败:', error);
|
||||||
|
toast.error('更新失败');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除参数
|
||||||
|
function deleteParameter(state: DeviceParameterState, paramKey: string): DeviceParameterState {
|
||||||
|
try {
|
||||||
|
if (!state.selectedType) return state;
|
||||||
|
|
||||||
|
const updatedParams = state.selectedType.parameterDefinitions.filter(p => p.key !== paramKey);
|
||||||
|
const updatedType: DeviceType = {
|
||||||
|
...state.selectedType,
|
||||||
|
parameterDefinitions: updatedParams,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTypes = state.deviceTypes.map(t =>
|
||||||
|
t.id === state.selectedType?.id ? updatedType : t
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(updatedTypes));
|
||||||
|
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(updatedTypes));
|
||||||
|
toast.success('参数删除成功');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: updatedTypes,
|
||||||
|
selectedType: updatedType,
|
||||||
|
showDeleteConfirm: false,
|
||||||
|
pendingDeleteParam: null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
toast.error('删除失败');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备参数管理页面 - IoT设备参数模板配置中心
|
||||||
|
* 功能:设备类型选择、参数模板配置、参数对比分析、报告生成
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-parameter
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Plus, Edit, Trash2, Eye } from 'lucide-react';
|
||||||
|
import { DeviceParameterStats } from './components/DeviceParameterStats';
|
||||||
|
import { DeviceParameterTable } from './components/DeviceParameterTable';
|
||||||
|
import { DeviceTypeSelector } from './components/DeviceTypeSelector';
|
||||||
|
import { deviceParameterReducer, initialState, DeviceParameterState } from './components/deviceParameterReducer';
|
||||||
|
import { AddParameterDialog } from './components/AddParameterDialog';
|
||||||
|
import { ViewParameterDialog } from './components/ViewParameterDialog';
|
||||||
|
import { DeleteParameterConfirmDialog } from './components/DeleteParameterConfirmDialog';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function DeviceParameterPage() {
|
||||||
|
const [state, dispatch] = useReducer(deviceParameterReducer, initialState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'LOAD_DATA' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddParam = () => {
|
||||||
|
if (!state.selectedType) {
|
||||||
|
toast.error('请先选择一个设备类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch({ type: 'SHOW_ADD_PARAM_DIALOG' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditParam = (param: any) => {
|
||||||
|
dispatch({ type: 'SHOW_EDIT_PARAM_DIALOG', payload: param });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewParam = (param: any) => {
|
||||||
|
dispatch({ type: 'SHOW_VIEW_PARAM_DIALOG', payload: param });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteParam = (paramKey: string) => {
|
||||||
|
dispatch({ type: 'SHOW_DELETE_CONFIRM', payload: paramKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-primary">设备参数管理</h2>
|
||||||
|
<p className="text-muted-foreground">为设备类型配置参数模板</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleAddParam}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
新增参数
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<DeviceParameterStats state={state} />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-12 gap-6">
|
||||||
|
{/* 左侧:设备类型选择器 */}
|
||||||
|
<div className="col-span-4">
|
||||||
|
<DeviceTypeSelector
|
||||||
|
state={state}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:参数列表 */}
|
||||||
|
<div className="col-span-8">
|
||||||
|
<Card className="bg-card">
|
||||||
|
<DeviceParameterTable
|
||||||
|
state={state}
|
||||||
|
onEdit={handleEditParam}
|
||||||
|
onView={handleViewParam}
|
||||||
|
onDelete={handleDeleteParam}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 添加/编辑参数对话框 */}
|
||||||
|
<AddParameterDialog
|
||||||
|
open={state.showAddParamDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_ADD_PARAM_DIALOG', payload: open })}
|
||||||
|
editingParam={state.editingParam}
|
||||||
|
selectedType={state.selectedType}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 查看参数对话框 */}
|
||||||
|
<ViewParameterDialog
|
||||||
|
open={state.showViewParamDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_VIEW_PARAM_DIALOG', payload: open })}
|
||||||
|
viewingParam={state.viewingParam}
|
||||||
|
selectedType={state.selectedType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<DeleteParameterConfirmDialog
|
||||||
|
open={state.showDeleteConfirm}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_DELETE_CONFIRM', payload: open })}
|
||||||
|
pendingDeleteParam={state.pendingDeleteParam}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 添加/编辑设备类型对话框 - 设备类型信息录入组件
|
||||||
|
* 功能:新增设备类型、编辑设备类型信息、表单验证
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
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 { DeviceType, DeviceTypeAction } from './deviceTypeReducer';
|
||||||
|
|
||||||
|
interface AddDeviceTypeDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
editingType: DeviceType | null;
|
||||||
|
dispatch: React.Dispatch<DeviceTypeAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddDeviceTypeDialog({ open, onOpenChange, editingType, dispatch }: AddDeviceTypeDialogProps) {
|
||||||
|
const { register, handleSubmit, setValue, reset, formState: { errors } } = useForm();
|
||||||
|
|
||||||
|
// 当编辑类型变化时,填充表单
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (editingType) {
|
||||||
|
setValue('name', editingType.name);
|
||||||
|
setValue('manufacturer', editingType.manufacturer || '');
|
||||||
|
setValue('model', editingType.model || '');
|
||||||
|
setValue('description', editingType.description || '');
|
||||||
|
} else {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}, [editingType, setValue, reset]);
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
if (editingType) {
|
||||||
|
const updated: DeviceType = {
|
||||||
|
...editingType,
|
||||||
|
...data,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
dispatch({ type: 'UPDATE_DEVICE_TYPE', payload: updated });
|
||||||
|
} else {
|
||||||
|
const newType: DeviceType = {
|
||||||
|
id: `device-type-${Date.now()}`,
|
||||||
|
...data,
|
||||||
|
parameterDefinitions: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
dispatch({ type: 'ADD_DEVICE_TYPE', payload: newType });
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
onOpenChange(open);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingType ? '编辑设备类型' : '新增设备类型'}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
填写设备类型的基本信息
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">类型名称 *</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
{...register('name', { required: '请输入类型名称' })}
|
||||||
|
placeholder="例如:土壤传感器"
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-sm text-destructive">{errors.name.message as string}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="manufacturer">品牌</Label>
|
||||||
|
<Input
|
||||||
|
id="manufacturer"
|
||||||
|
{...register('manufacturer')}
|
||||||
|
placeholder="例如:施耐德"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="model">型号</Label>
|
||||||
|
<Input
|
||||||
|
id="model"
|
||||||
|
{...register('model')}
|
||||||
|
placeholder="例如:SOIL-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">描述</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
{...register('description')}
|
||||||
|
placeholder="设备类型的详细描述..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{editingType ? '保存' : '添加'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 删除确认对话框 - 删除操作确认组件
|
||||||
|
* 功能:确认删除设备类型、防止误操作、影响提示
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||||
|
import { DeviceTypeAction } from './deviceTypeReducer';
|
||||||
|
|
||||||
|
interface DeleteConfirmDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
pendingDeleteId: string | null;
|
||||||
|
dispatch: React.Dispatch<DeviceTypeAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteConfirmDialog({ open, onOpenChange, pendingDeleteId, dispatch }: DeleteConfirmDialogProps) {
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (pendingDeleteId) {
|
||||||
|
dispatch({ type: 'DELETE_DEVICE_TYPE', payload: pendingDeleteId });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
确定要删除此设备类型吗?此操作不可撤销。
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => onOpenChange(false)}>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={confirmDelete}>删除</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备类型统计组件 - 统计卡片展示组件
|
||||||
|
* 功能:显示设备类型统计数据、参数配置情况、品牌型号覆盖率、分类统计
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { DeviceTypeState } from './deviceTypeReducer';
|
||||||
|
|
||||||
|
interface DeviceTypeStatsProps {
|
||||||
|
state: DeviceTypeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceTypeStats({ state }: DeviceTypeStatsProps) {
|
||||||
|
const totalParams = state.deviceTypes.reduce((sum, type) => sum + (type.parameterDefinitions?.length || 0), 0);
|
||||||
|
const typesWithParams = state.deviceTypes.filter(type => (type.parameterDefinitions?.length || 0) > 0).length;
|
||||||
|
const typesWithManufacturer = state.deviceTypes.filter(t => t.manufacturer).length;
|
||||||
|
const typesWithModel = state.deviceTypes.filter(t => t.model).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
|
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
||||||
|
<div className="text-sm text-muted-foreground">总类型数</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400">
|
||||||
|
{state.deviceTypes.length}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="text-sm text-muted-foreground">参数模板总数</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-blue-600 dark:text-blue-400">
|
||||||
|
{totalParams}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
|
||||||
|
<div className="text-sm text-muted-foreground">已配置模板</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-orange-600 dark:text-orange-400">
|
||||||
|
{typesWithParams} / {state.deviceTypes.length}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="text-sm text-muted-foreground">已填写品牌</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-purple-600 dark:text-purple-400">
|
||||||
|
{typesWithManufacturer}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-pink-50 dark:bg-pink-950 border-pink-200 dark:border-pink-800">
|
||||||
|
<div className="text-sm text-muted-foreground">已填写型号</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-pink-600 dark:text-pink-400">
|
||||||
|
{typesWithModel}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备类型表格组件 - 数据列表展示组件
|
||||||
|
* 功能:设备类型列表展示、操作按钮、参数模板链接、分类标签
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Eye, Edit, Trash2, Settings } from 'lucide-react';
|
||||||
|
import { DeviceTypeState, DeviceType } from './deviceTypeReducer';
|
||||||
|
|
||||||
|
interface DeviceTypeTableProps {
|
||||||
|
state: DeviceTypeState;
|
||||||
|
onEdit: (deviceType: DeviceType) => void;
|
||||||
|
onView: (deviceType: DeviceType) => void;
|
||||||
|
onViewParams: (deviceType: DeviceType) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceTypeTable({ state, onEdit, onView, onViewParams, onDelete }: DeviceTypeTableProps) {
|
||||||
|
const getProtocolColor = (protocol?: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'LoRaWAN': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
'MQTT': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||||
|
'Modbus RTU': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||||
|
'HTTP': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||||
|
};
|
||||||
|
return colors[protocol || ''] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryColor = (category?: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'环境监测': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
'气象监测': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||||
|
'灌溉控制': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||||
|
'土壤监测': 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
|
||||||
|
};
|
||||||
|
return colors[category || ''] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>类型名称</TableHead>
|
||||||
|
<TableHead>品牌</TableHead>
|
||||||
|
<TableHead>型号</TableHead>
|
||||||
|
<TableHead>描述</TableHead>
|
||||||
|
<TableHead>参数模板</TableHead>
|
||||||
|
<TableHead className="text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{state.deviceTypes.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||||
|
暂无数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
state.deviceTypes.map(type => (
|
||||||
|
<TableRow key={type.id}>
|
||||||
|
<TableCell className="font-medium">{type.name}</TableCell>
|
||||||
|
<TableCell>{type.manufacturer || '-'}</TableCell>
|
||||||
|
<TableCell>{type.model || '-'}</TableCell>
|
||||||
|
<TableCell className="max-w-md">
|
||||||
|
<div className="truncate">
|
||||||
|
{type.description || '-'}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{(type.parameterDefinitions?.length || 0) > 0 ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onViewParams(type)}
|
||||||
|
className="text-primary hover:text-primary/80 p-1 h-auto"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 mr-1" />
|
||||||
|
<span className="text-sm">{type.parameterDefinitions.length} 个参数</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-sm">未定义</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onView(type)}
|
||||||
|
title="查看"
|
||||||
|
className="p-1 h-auto"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onEdit(type)}
|
||||||
|
title="编辑"
|
||||||
|
className="p-1 h-auto"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDelete(type.id)}
|
||||||
|
title="删除"
|
||||||
|
className="p-1 h-auto"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 查看设备类型详情对话框 - 设备类型详情展示组件
|
||||||
|
* 功能:展示设备类型完整信息、参数配置、技术规格
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { DeviceType } from './deviceTypeReducer';
|
||||||
|
|
||||||
|
interface ViewDeviceTypeDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
viewingType: DeviceType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewDeviceTypeDialog({ open, onOpenChange, viewingType }: ViewDeviceTypeDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>设备类型详情</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{viewingType && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-muted-foreground">类型名称</label>
|
||||||
|
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.name}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-muted-foreground">品牌</label>
|
||||||
|
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.manufacturer || '-'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-muted-foreground">型号</label>
|
||||||
|
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.model || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-muted-foreground">参数模板数</label>
|
||||||
|
<div className="field-value mt-1 p-2 bg-muted rounded">
|
||||||
|
{viewingType.parameterDefinitions?.length || 0} 个
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-muted-foreground">描述</label>
|
||||||
|
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.description || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-muted-foreground">创建时间</label>
|
||||||
|
<div className="field-value mt-1 p-2 bg-muted rounded">
|
||||||
|
{new Date(viewingType.createdAt).toLocaleString('zh-CN')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-muted-foreground">更新时间</label>
|
||||||
|
<div className="field-value mt-1 p-2 bg-muted rounded">
|
||||||
|
{new Date(viewingType.updatedAt).toLocaleString('zh-CN')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={() => onOpenChange(false)}>关闭</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 查看参数模板对话框 - 参数配置详情展示组件
|
||||||
|
* 功能:展示设备类型参数定义、参数类型和配置详情
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { DeviceType } from './deviceTypeReducer';
|
||||||
|
|
||||||
|
interface ViewParamsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
viewingParams: DeviceType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewParamsDialog({ open, onOpenChange, viewingParams }: ViewParamsDialogProps) {
|
||||||
|
const getParamTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'number': '数字',
|
||||||
|
'string': '文本',
|
||||||
|
'boolean': '布尔',
|
||||||
|
'select': '选择'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>参数模板 - {viewingParams?.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
该设备类型的参数定义
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{viewingParams && viewingParams.parameterDefinitions.length > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>参数标识</TableHead>
|
||||||
|
<TableHead>参数名称</TableHead>
|
||||||
|
<TableHead>数据类型</TableHead>
|
||||||
|
<TableHead>必填</TableHead>
|
||||||
|
<TableHead>默认值</TableHead>
|
||||||
|
<TableHead>单位</TableHead>
|
||||||
|
<TableHead>范围</TableHead>
|
||||||
|
<TableHead>说明</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{viewingParams.parameterDefinitions.map((param, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell className="font-mono text-sm">{param.key}</TableCell>
|
||||||
|
<TableCell>{param.label}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline">{getParamTypeLabel(param.type)}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{param.required ? (
|
||||||
|
<Badge className="bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800">是</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">否</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{param.type === 'boolean'
|
||||||
|
? (param.defaultValue ? '是' : '否')
|
||||||
|
: (param.defaultValue?.toString() || '-')
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{param.unit || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{param.min !== undefined && param.max !== undefined
|
||||||
|
? `${param.min} ~ ${param.max}`
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="max-w-xs">
|
||||||
|
<div className="truncate" title={param.description}>
|
||||||
|
{param.description || '-'}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground py-8">
|
||||||
|
该设备类型暂无参数定义
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={() => onOpenChange(false)}>关闭</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备类型状态管理 - 集中化状态管理核心
|
||||||
|
* 功能:设备类型数据管理、弹窗状态控制、筛选条件管理
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type/components
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer模式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 参数定义接口
|
||||||
|
export interface ParameterDefinition {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'select';
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
options?: { label: string; value: any }[];
|
||||||
|
unit?: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备类型接口
|
||||||
|
export interface DeviceType {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
model?: string;
|
||||||
|
category?: string;
|
||||||
|
description?: string;
|
||||||
|
protocol?: string;
|
||||||
|
connectivity?: string;
|
||||||
|
powerSupply?: string;
|
||||||
|
operatingTemperature?: string;
|
||||||
|
protectionRating?: string;
|
||||||
|
parameterDefinitions: ParameterDefinition[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 状态接口
|
||||||
|
export interface DeviceTypeState {
|
||||||
|
deviceTypes: DeviceType[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
showAddDialog: boolean;
|
||||||
|
showEditDialog: boolean;
|
||||||
|
showViewDialog: boolean;
|
||||||
|
showParamsDialog: boolean;
|
||||||
|
showDeleteDialog: boolean;
|
||||||
|
|
||||||
|
// 编辑/查看数据
|
||||||
|
editingType: DeviceType | null;
|
||||||
|
viewingType: DeviceType | null;
|
||||||
|
viewingParams: DeviceType | null;
|
||||||
|
pendingDeleteId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action类型定义
|
||||||
|
export type DeviceTypeAction =
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_ERROR'; payload: string | null }
|
||||||
|
| { type: 'SET_DEVICE_TYPES'; payload: DeviceType[] }
|
||||||
|
| { type: 'ADD_DEVICE_TYPE'; payload: DeviceType }
|
||||||
|
| { type: 'UPDATE_DEVICE_TYPE'; payload: DeviceType }
|
||||||
|
| { type: 'DELETE_DEVICE_TYPE'; payload: string }
|
||||||
|
| { type: 'SHOW_ADD_DIALOG' }
|
||||||
|
| { type: 'SHOW_EDIT_DIALOG'; payload: DeviceType }
|
||||||
|
| { type: 'SHOW_VIEW_DIALOG'; payload: DeviceType }
|
||||||
|
| { type: 'SHOW_PARAMS_DIALOG'; payload: DeviceType }
|
||||||
|
| { type: 'SHOW_DELETE_DIALOG'; payload: string }
|
||||||
|
| { type: 'SET_ADD_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_EDIT_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_VIEW_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_PARAMS_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_DELETE_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'LOAD_DATA' };
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
export const initialState: DeviceTypeState = {
|
||||||
|
deviceTypes: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
showAddDialog: false,
|
||||||
|
showEditDialog: false,
|
||||||
|
showViewDialog: false,
|
||||||
|
showParamsDialog: false,
|
||||||
|
showDeleteDialog: false,
|
||||||
|
editingType: null,
|
||||||
|
viewingType: null,
|
||||||
|
viewingParams: null,
|
||||||
|
pendingDeleteId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reducer函数
|
||||||
|
export function deviceTypeReducer(state: DeviceTypeState, action: DeviceTypeAction): DeviceTypeState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return { ...state, loading: action.payload };
|
||||||
|
|
||||||
|
case 'SET_ERROR':
|
||||||
|
return { ...state, error: action.payload, loading: false };
|
||||||
|
|
||||||
|
case 'SET_DEVICE_TYPES':
|
||||||
|
return { ...state, deviceTypes: action.payload, loading: false };
|
||||||
|
|
||||||
|
case 'ADD_DEVICE_TYPE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: [...state.deviceTypes, action.payload],
|
||||||
|
showAddDialog: false,
|
||||||
|
editingType: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_DEVICE_TYPE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: state.deviceTypes.map(type =>
|
||||||
|
type.id === action.payload.id ? action.payload : type
|
||||||
|
),
|
||||||
|
showEditDialog: false,
|
||||||
|
showAddDialog: false,
|
||||||
|
editingType: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'DELETE_DEVICE_TYPE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: state.deviceTypes.filter(type => type.id !== action.payload),
|
||||||
|
showDeleteDialog: false,
|
||||||
|
pendingDeleteId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_ADD_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAddDialog: true,
|
||||||
|
editingType: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_EDIT_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showEditDialog: true,
|
||||||
|
editingType: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_VIEW_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showViewDialog: true,
|
||||||
|
viewingType: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_PARAMS_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showParamsDialog: true,
|
||||||
|
viewingParams: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_DELETE_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showDeleteDialog: true,
|
||||||
|
pendingDeleteId: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_ADD_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAddDialog: action.payload,
|
||||||
|
editingType: action.payload ? null : state.editingType,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_EDIT_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showEditDialog: action.payload,
|
||||||
|
editingType: action.payload ? null : state.editingType,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_VIEW_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showViewDialog: action.payload,
|
||||||
|
viewingType: action.payload ? null : state.viewingType,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_PARAMS_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showParamsDialog: action.payload,
|
||||||
|
viewingParams: action.payload ? null : state.viewingParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_DELETE_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showDeleteDialog: action.payload,
|
||||||
|
pendingDeleteId: action.payload ? null : state.pendingDeleteId,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'LOAD_DATA':
|
||||||
|
return loadData(state);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
function loadData(state: DeviceTypeState): DeviceTypeState {
|
||||||
|
const savedData = localStorage.getItem('smart_agriculture_ai_device_types');
|
||||||
|
if (savedData) {
|
||||||
|
try {
|
||||||
|
const deviceTypes = JSON.parse(savedData);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load device types:', error);
|
||||||
|
// 如果加载失败,初始化测试数据
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: initializeTestData(),
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 初始化测试数据
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceTypes: initializeTestData(),
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化测试数据
|
||||||
|
function initializeTestData(): DeviceType[] {
|
||||||
|
const testDeviceTypes: DeviceType[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: '智能土壤监测传感器',
|
||||||
|
manufacturer: 'GreenTech',
|
||||||
|
model: 'GT-SS-100',
|
||||||
|
category: '环境监测',
|
||||||
|
description: '用于实时监测土壤温度、湿度、pH值等环境参数的多功能传感器',
|
||||||
|
protocol: 'LoRaWAN',
|
||||||
|
connectivity: '无线',
|
||||||
|
powerSupply: '太阳能+电池',
|
||||||
|
operatingTemperature: '-30°C ~ 70°C',
|
||||||
|
protectionRating: 'IP67',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'temperature',
|
||||||
|
label: '土壤温度',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
unit: '°C',
|
||||||
|
min: -30,
|
||||||
|
max: 70,
|
||||||
|
description: '土壤温度监测,用于判断作物生长环境',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'humidity',
|
||||||
|
label: '土壤湿度',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
unit: '%',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
description: '土壤相对湿度百分比',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ph',
|
||||||
|
label: 'pH值',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
unit: 'pH',
|
||||||
|
min: 0,
|
||||||
|
max: 14,
|
||||||
|
defaultValue: 7,
|
||||||
|
description: '土壤酸碱度,影响作物养分吸收',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdAt: '2024-01-15T08:00:00Z',
|
||||||
|
updatedAt: '2024-01-15T08:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: '智能气象站',
|
||||||
|
manufacturer: 'WeatherPro',
|
||||||
|
model: 'WP-WS-200',
|
||||||
|
category: '气象监测',
|
||||||
|
description: '综合气象监测设备,可监测温度、湿度、风速、风向、降雨量等',
|
||||||
|
protocol: 'MQTT',
|
||||||
|
connectivity: '4G/WiFi',
|
||||||
|
powerSupply: '市电+电池',
|
||||||
|
operatingTemperature: '-40°C ~ 85°C',
|
||||||
|
protectionRating: 'IP66',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'air_temp',
|
||||||
|
label: '空气温度',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
unit: '°C',
|
||||||
|
min: -40,
|
||||||
|
max: 85,
|
||||||
|
description: '环境空气温度',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'air_humidity',
|
||||||
|
label: '空气湿度',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
unit: '%',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
description: '相对空气湿度',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'wind_speed',
|
||||||
|
label: '风速',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
unit: 'm/s',
|
||||||
|
min: 0,
|
||||||
|
max: 50,
|
||||||
|
description: '风速监测',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rainfall',
|
||||||
|
label: '降雨量',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
unit: 'mm',
|
||||||
|
min: 0,
|
||||||
|
max: 500,
|
||||||
|
defaultValue: 0,
|
||||||
|
description: '累计降雨量',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdAt: '2024-01-16T09:30:00Z',
|
||||||
|
updatedAt: '2024-01-16T09:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: '智能灌溉控制器',
|
||||||
|
manufacturer: 'IrrigationTech',
|
||||||
|
model: 'IT-IC-300',
|
||||||
|
category: '灌溉控制',
|
||||||
|
description: '自动化灌溉控制系统,支持定时灌溉、土壤湿度阈值控制等多种模式',
|
||||||
|
protocol: 'Modbus RTU',
|
||||||
|
connectivity: 'RS485',
|
||||||
|
powerSupply: 'DC 12V',
|
||||||
|
operatingTemperature: '-20°C ~ 60°C',
|
||||||
|
protectionRating: 'IP65',
|
||||||
|
parameterDefinitions: [
|
||||||
|
{
|
||||||
|
key: 'valve_status',
|
||||||
|
label: '阀门状态',
|
||||||
|
type: 'boolean',
|
||||||
|
required: true,
|
||||||
|
defaultValue: false,
|
||||||
|
description: '灌溉阀门开启/关闭状态',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'flow_rate',
|
||||||
|
label: '流量',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
unit: 'L/min',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
defaultValue: 0,
|
||||||
|
description: '实时流量监测',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'irrigation_mode',
|
||||||
|
label: '灌溉模式',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: '手动控制', value: 'manual' },
|
||||||
|
{ label: '定时灌溉', value: 'scheduled' },
|
||||||
|
{ label: '湿度控制', value: 'humidity' },
|
||||||
|
{ label: '智能控制', value: 'smart' },
|
||||||
|
],
|
||||||
|
defaultValue: 'manual',
|
||||||
|
description: '灌溉控制模式选择',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdAt: '2024-01-17T10:15:00Z',
|
||||||
|
updatedAt: '2024-01-17T10:15:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(testDeviceTypes));
|
||||||
|
|
||||||
|
return testDeviceTypes;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 设备类型管理页面 - IoT设备类型定义与参数管理中心
|
||||||
|
* 功能:设备类型列表管理、参数模板配置、设备类型统计
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/device-type
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { DeviceTypeTable } from './components/DeviceTypeTable';
|
||||||
|
import { DeviceTypeStats } from './components/DeviceTypeStats';
|
||||||
|
import { deviceTypeReducer, initialState, DeviceTypeState } from './components/deviceTypeReducer';
|
||||||
|
import { AddDeviceTypeDialog } from './components/AddDeviceTypeDialog';
|
||||||
|
import { ViewDeviceTypeDialog } from './components/ViewDeviceTypeDialog';
|
||||||
|
import { ViewParamsDialog } from './components/ViewParamsDialog';
|
||||||
|
import { DeleteConfirmDialog } from './components/DeleteConfirmDialog';
|
||||||
|
|
||||||
|
export default function DeviceTypePage() {
|
||||||
|
const [state, dispatch] = useReducer(deviceTypeReducer, initialState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'LOAD_DATA' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
dispatch({ type: 'SHOW_ADD_DIALOG' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (deviceType: any) => {
|
||||||
|
dispatch({ type: 'SHOW_EDIT_DIALOG', payload: deviceType });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (deviceType: any) => {
|
||||||
|
dispatch({ type: 'SHOW_VIEW_DIALOG', payload: deviceType });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewParams = (deviceType: any) => {
|
||||||
|
dispatch({ type: 'SHOW_PARAMS_DIALOG', payload: deviceType });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
dispatch({ type: 'SHOW_DELETE_DIALOG', payload: id });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-primary">设备类型管理</h2>
|
||||||
|
<p className="text-muted-foreground">定义和管理物联设备的类型信息</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleAdd}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
新增类型
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<DeviceTypeStats state={state} />
|
||||||
|
|
||||||
|
{/* 设备类型列表 */}
|
||||||
|
<Card>
|
||||||
|
<DeviceTypeTable
|
||||||
|
state={state}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onView={handleView}
|
||||||
|
onViewParams={handleViewParams}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 添加/编辑对话框 */}
|
||||||
|
<AddDeviceTypeDialog
|
||||||
|
open={state.showAddDialog || state.showEditDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open) {
|
||||||
|
dispatch({ type: 'SET_ADD_DIALOG', payload: true });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_ADD_DIALOG', payload: false });
|
||||||
|
dispatch({ type: 'SET_EDIT_DIALOG', payload: false });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
editingType={state.editingType}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 查看详情对话框 */}
|
||||||
|
<ViewDeviceTypeDialog
|
||||||
|
open={state.showViewDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_VIEW_DIALOG', payload: open })}
|
||||||
|
viewingType={state.viewingType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 查看参数模板对话框 */}
|
||||||
|
<ViewParamsDialog
|
||||||
|
open={state.showParamsDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_PARAMS_DIALOG', payload: open })}
|
||||||
|
viewingParams={state.viewingParams}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
open={state.showDeleteDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_DELETE_DIALOG', payload: open })}
|
||||||
|
pendingDeleteId={state.pendingDeleteId}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -177,7 +177,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
// 每 1 分钟刷新一次 token
|
// 每 1 分钟刷新一次 token
|
||||||
refreshTimerRef.current = setInterval(() => {
|
refreshTimerRef.current = setInterval(() => {
|
||||||
refreshAccessToken();
|
refreshAccessToken();
|
||||||
}, 5 * 60 * 1000); // 60 秒 = 1 分钟
|
}, 5 * 1000); // 60 秒 = 1 分钟
|
||||||
|
|
||||||
console.log('🕐 Token 自动刷新定时器已启动(每5分钟)');
|
console.log('🕐 Token 自动刷新定时器已启动(每5分钟)');
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user