生产管理系统 - 设备类型管理、设备参数管理

This commit is contained in:
2025-11-01 11:46:13 +08:00
parent cb46f91846
commit 3459cae699
17 changed files with 2806 additions and 1 deletions

View File

@@ -0,0 +1,465 @@
/**
* filekorolheader: 添加/编辑参数对话框 - 参数模板配置组件
* 功能:新增参数、编辑参数信息、表单验证、不同类型参数配置
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { ParameterDefinition, DeviceType, DeviceParameterAction } from './deviceParameterReducer';
import { Save, X, Plus } from 'lucide-react';
import { toast } from 'sonner';
interface AddParameterDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingParam: ParameterDefinition | null;
selectedType: DeviceType | null;
dispatch: React.Dispatch<DeviceParameterAction>;
}
interface ParamForm {
key: string;
label: string;
type: 'string' | 'number' | 'boolean' | 'select';
required: boolean;
defaultValue: string;
unit: string;
min: string;
max: string;
description: string;
options: { label: string; value: string }[];
}
export function AddParameterDialog({ open, onOpenChange, editingParam, selectedType, dispatch }: AddParameterDialogProps) {
const [paramForm, setParamForm] = useState<ParamForm>({
key: '',
label: '',
type: 'string',
required: false,
defaultValue: '',
unit: '',
min: '',
max: '',
description: '',
options: []
});
const [optionLabel, setOptionLabel] = useState('');
const [optionValue, setOptionValue] = useState('');
const [errors, setErrors] = useState<Partial<Record<keyof ParamForm, string>>>({});
useEffect(() => {
if (editingParam) {
setParamForm({
key: editingParam.key,
label: editingParam.label,
type: editingParam.type,
required: editingParam.required || false,
defaultValue: editingParam.defaultValue?.toString() || '',
unit: editingParam.unit || '',
min: editingParam.min?.toString() || '',
max: editingParam.max?.toString() || '',
description: editingParam.description || '',
options: editingParam.options || []
});
} else {
setParamForm({
key: '',
label: '',
type: 'string',
required: false,
defaultValue: '',
unit: '',
min: '',
max: '',
description: '',
options: []
});
}
setErrors({});
setOptionLabel('');
setOptionValue('');
}, [editingParam, open]);
const validateForm = (): boolean => {
const newErrors: Partial<Record<keyof ParamForm, string>> = {};
if (!paramForm.key.trim()) {
newErrors.key = '请输入参数标识';
}
if (!paramForm.label.trim()) {
newErrors.label = '请输入参数名称';
}
// 检查参数标识是否重复(编辑时除外)
if (!editingParam || editingParam.key !== paramForm.key) {
const exists = selectedType?.parameterDefinitions?.some(p => p.key === paramForm.key.trim());
if (exists) {
newErrors.key = '参数标识已存在';
}
}
// 验证数字类型的范围
if (paramForm.type === 'number') {
if (paramForm.min && paramForm.max) {
const min = parseFloat(paramForm.min);
const max = parseFloat(paramForm.max);
if (min >= max) {
newErrors.max = '最大值必须大于最小值';
}
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSaveParam = () => {
if (!selectedType) return;
if (!validateForm()) {
return;
}
try {
// 构建新参数
const newParam: ParameterDefinition = {
key: paramForm.key.trim(),
label: paramForm.label.trim(),
type: paramForm.type,
required: paramForm.required,
description: paramForm.description.trim()
};
// 根据类型设置默认值和属性
if (paramForm.type === 'number') {
newParam.defaultValue = paramForm.defaultValue ? parseFloat(paramForm.defaultValue) : 0;
if (paramForm.unit) newParam.unit = paramForm.unit;
if (paramForm.min) newParam.min = parseFloat(paramForm.min);
if (paramForm.max) newParam.max = parseFloat(paramForm.max);
} else if (paramForm.type === 'boolean') {
newParam.defaultValue = paramForm.defaultValue === 'true' || paramForm.defaultValue === true;
} else if (paramForm.type === 'select') {
newParam.options = paramForm.options;
newParam.defaultValue = paramForm.defaultValue || (paramForm.options[0]?.value || '');
} else {
newParam.defaultValue = paramForm.defaultValue;
}
if (editingParam) {
dispatch({ type: 'UPDATE_PARAMETER', payload: newParam });
} else {
dispatch({ type: 'ADD_PARAMETER', payload: newParam });
}
onOpenChange(false);
} catch (error) {
console.error('保存失败:', error);
toast.error('保存失败,请重试');
}
};
const handleAddOption = () => {
if (!optionLabel.trim() || !optionValue.trim()) {
toast.error('请填写选项标签和值');
return;
}
const newOption = { label: optionLabel.trim(), value: optionValue.trim() };
setParamForm({
...paramForm,
options: [...paramForm.options, newOption]
});
setOptionLabel('');
setOptionValue('');
};
const handleRemoveOption = (index: number) => {
setParamForm({
...paramForm,
options: paramForm.options.filter((_, i) => i !== index)
});
};
const handleInputChange = (field: keyof ParamForm, value: any) => {
setParamForm(prev => ({ ...prev, [field]: value }));
// 清除该字段的错误
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
{editingParam ? '编辑参数' : '新增参数'}
</DialogTitle>
<DialogDescription>
{selectedType?.name}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 基本信息 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="key" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="key"
value={paramForm.key}
onChange={(e) => handleInputChange('key', e.target.value)}
placeholder="例如temperature"
disabled={!!editingParam}
className={errors.key ? 'border-red-500' : ''}
/>
{errors.key && (
<p className="text-sm text-red-500">{errors.key}</p>
)}
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="label" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="label"
value={paramForm.label}
onChange={(e) => handleInputChange('label', e.target.value)}
placeholder="例如:温度"
className={errors.label ? 'border-red-500' : ''}
/>
{errors.label && (
<p className="text-sm text-red-500">{errors.label}</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="type" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Select
value={paramForm.type}
onValueChange={(value: any) => handleInputChange('type', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="boolean"></SelectItem>
<SelectItem value="select"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="required" className="text-sm font-medium">
</Label>
<div className="flex items-center space-x-2 h-10">
<Switch
id="required"
checked={paramForm.required}
onCheckedChange={(checked) => handleInputChange('required', checked)}
/>
<Label htmlFor="required" className="cursor-pointer text-sm">
{paramForm.required ? '必填' : '选填'}
</Label>
</div>
</div>
</div>
{/* 根据类型显示不同字段 */}
{paramForm.type === 'number' && (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="unit" className="text-sm font-medium">
</Label>
<Input
id="unit"
value={paramForm.unit}
onChange={(e) => handleInputChange('unit', e.target.value)}
placeholder="例如°C"
/>
</div>
<div className="space-y-2">
<Label htmlFor="min" className="text-sm font-medium">
</Label>
<Input
id="min"
type="number"
value={paramForm.min}
onChange={(e) => handleInputChange('min', e.target.value)}
placeholder="最小值"
/>
</div>
<div className="space-y-2">
<Label htmlFor="max" className="text-sm font-medium">
</Label>
<Input
id="max"
type="number"
value={paramForm.max}
onChange={(e) => handleInputChange('max', e.target.value)}
placeholder="最大值"
className={errors.max ? 'border-red-500' : ''}
/>
{errors.max && (
<p className="text-sm text-red-500">{errors.max}</p>
)}
</div>
</div>
)}
{/* 选择类型的选项配置 */}
{paramForm.type === 'select' && (
<div className="space-y-2">
<Label className="text-sm font-medium"></Label>
<div className="border rounded-lg p-4 space-y-3 bg-muted/30">
<div className="flex gap-2">
<Input
value={optionLabel}
onChange={(e) => setOptionLabel(e.target.value)}
placeholder="选项标签(显示文本)"
className="flex-1"
/>
<Input
value={optionValue}
onChange={(e) => setOptionValue(e.target.value)}
placeholder="选项值"
className="flex-1"
/>
<Button onClick={handleAddOption} size="sm">
<Plus className="w-4 h-4" />
</Button>
</div>
{paramForm.options.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground"></p>
{paramForm.options.map((option, index) => (
<div key={index} className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm">
{option.label} <span className="text-muted-foreground">({option.value})</span>
</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveOption(index)}
className="h-6 w-6 p-0"
>
<X className="w-3 h-3" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* 默认值 */}
<div className="space-y-2">
<Label htmlFor="defaultValue" className="text-sm font-medium">
</Label>
{paramForm.type === 'boolean' ? (
<Select
value={paramForm.defaultValue?.toString() || 'false'}
onValueChange={(value) => handleInputChange('defaultValue', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
) : paramForm.type === 'select' && paramForm.options.length > 0 ? (
<Select
value={paramForm.defaultValue}
onValueChange={(value) => handleInputChange('defaultValue', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{paramForm.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id="defaultValue"
type={paramForm.type === 'number' ? 'number' : 'text'}
value={paramForm.defaultValue}
onChange={(e) => handleInputChange('defaultValue', e.target.value)}
placeholder="参数默认值"
/>
)}
</div>
{/* 描述 */}
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
</Label>
<Textarea
id="description"
value={paramForm.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="参数说明..."
rows={3}
/>
</div>
</div>
<DialogFooter className="flex justify-between sm:justify-between">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="px-6"
>
<X className="w-4 h-4 mr-2" />
</Button>
<Button
type="button"
onClick={handleSaveParam}
className="px-6"
>
<Save className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,502 @@
/**
* filekorolheader: 设备参数管理状态管理器 - 参数模板配置中心
* 功能:设备类型数据管理、参数模板配置、对话框状态控制、操作处理
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.mduseReducer状态管理模式
*/
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;
}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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分钟');
};