生产管理系统 - 设备类型管理、设备参数管理
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
|
||||
refreshTimerRef.current = setInterval(() => {
|
||||
refreshAccessToken();
|
||||
}, 5 * 60 * 1000); // 60 秒 = 1 分钟
|
||||
}, 5 * 1000); // 60 秒 = 1 分钟
|
||||
|
||||
console.log('🕐 Token 自动刷新定时器已启动(每5分钟)');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user