子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DeviceControlPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/application/device-control
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ExternalSystemPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/application/external-system
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ApplicationPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/application
</p>
</div>
</Card>
</div>
);
}

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

@@ -0,0 +1,238 @@
'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { ExternalDataState, ExternalDataAction } from './externalDataReducer';
import { DataSourceForm, DataSourceType, AccessMethod, accessMethods } from '../types';
import { Database, Plus, Upload, Code, Wifi } from 'lucide-react';
import { toast } from 'sonner';
interface AddDataSourceDialogProps {
state: ExternalDataState;
dispatch: React.Dispatch<ExternalDataAction>;
}
export function AddDataSourceDialog({ state, dispatch }: AddDataSourceDialogProps) {
const [formData, setFormData] = useState<DataSourceForm>({
name: '',
type: '气象数据',
provider: '',
accessMethod: 'API对接',
apiEndpoint: '',
updateFrequency: '',
description: '',
});
const resetForm = () => {
setFormData({
name: '',
type: '气象数据',
provider: '',
accessMethod: 'API对接',
apiEndpoint: '',
updateFrequency: '',
description: '',
});
};
const handleSubmit = () => {
if (!formData.name || !formData.provider || !formData.updateFrequency) {
toast.error('请填写必要字段');
return;
}
const newDataSource = {
id: `ext-${Date.now()}`,
...formData,
lastUpdateTime: new Date().toLocaleString('zh-CN'),
dataPoints: 0,
status: '待配置' as const,
dataFields: [],
};
dispatch({ type: 'ADD_DATA_SOURCE', payload: newDataSource });
dispatch({ type: 'SHOW_ADD_DIALOG', payload: false });
resetForm();
toast.success('数据源添加成功');
};
const handleClose = () => {
dispatch({ type: 'SHOW_ADD_DIALOG', payload: false });
resetForm();
};
const getAccessMethodIcon = (method: AccessMethod) => {
switch (method) {
case 'API对接':
return <Code className="w-4 h-4" />;
case 'FTP传输':
return <Upload className="w-4 h-4" />;
case 'WebSocket':
return <Wifi className="w-4 h-4" />;
case '手动上传':
return <Upload className="w-4 h-4" />;
default:
return <Database className="w-4 h-4" />;
}
};
return (
<Dialog open={state.showAddDialog} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Plus className="w-5 h-5" />
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 基本信息 */}
<Card className="p-4 bg-muted/20">
<h3 className="font-medium mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例如国家气象局API"
/>
</div>
<div>
<Label htmlFor="provider"> *</Label>
<Input
id="provider"
value={formData.provider}
onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
placeholder="例如:中国气象局"
/>
</div>
</div>
</Card>
{/* 数据配置 */}
<Card className="p-4 bg-muted/20">
<h3 className="font-medium mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="type"></Label>
<Select
value={formData.type}
onValueChange={(value: DataSourceType) =>
setFormData({ ...formData, type: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="气象数据"></SelectItem>
<SelectItem value="卫星遥感"></SelectItem>
<SelectItem value="土壤数据"></SelectItem>
<SelectItem value="作物生长"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="updateFrequency"> *</Label>
<Input
id="updateFrequency"
value={formData.updateFrequency}
onChange={(e) => setFormData({ ...formData, updateFrequency: e.target.value })}
placeholder="例如每小时、每5天"
/>
</div>
</div>
</Card>
{/* 接入方式 */}
<Card className="p-4 bg-muted/20">
<h3 className="font-medium mb-4"></h3>
<div className="space-y-4">
<div>
<Label></Label>
<div className="grid grid-cols-2 gap-3 mt-2">
{accessMethods.map((method) => (
<Button
key={method}
type="button"
variant={formData.accessMethod === method ? 'default' : 'outline'}
className="justify-start h-auto p-3"
onClick={() => setFormData({ ...formData, accessMethod: method })}
>
<div className="flex items-center gap-2">
{getAccessMethodIcon(method)}
<div className="text-left">
<div className="font-medium text-sm">{method}</div>
</div>
</div>
</Button>
))}
</div>
</div>
{formData.accessMethod === 'API对接' && (
<div>
<Label htmlFor="apiEndpoint">API端点</Label>
<Input
id="apiEndpoint"
value={formData.apiEndpoint}
onChange={(e) => setFormData({ ...formData, apiEndpoint: e.target.value })}
placeholder="https://api.example.com/v1/data"
/>
</div>
)}
</div>
</Card>
{/* 描述 */}
<Card className="p-4 bg-muted/20">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="描述数据源的用途、数据内容等信息..."
rows={3}
/>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleClose}>
</Button>
<Button onClick={handleSubmit}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,197 @@
'use client';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ExternalDataSource, dataSourceTypes, dataSourceStatuses } from '../types';
import { ExternalDataAction } from './externalDataReducer';
import {
Database,
Eye,
Edit,
Trash2,
Cloud,
Code,
Upload,
Wifi,
CheckCircle,
XCircle,
AlertTriangle,
Clock,
Link,
} from 'lucide-react';
interface DataSourceCardProps {
dataSource: ExternalDataSource;
onView: (dataSource: ExternalDataSource) => void;
onEdit: (dataSource: ExternalDataSource) => void;
onDelete: (id: string) => void;
}
export function DataSourceCard({ dataSource, onView, onEdit, onDelete }: DataSourceCardProps) {
const getStatusIcon = (status: string) => {
switch (status) {
case '正常':
return <CheckCircle className="w-4 h-4 text-success" />;
case '异常':
return <XCircle className="w-4 h-4 text-destructive" />;
case '离线':
return <Clock className="w-4 h-4 text-muted-foreground" />;
case '待配置':
return <AlertTriangle className="w-4 h-4 text-warning" />;
default:
return <Clock className="w-4 h-4 text-muted-foreground" />;
}
};
const getAccessMethodIcon = (method: string) => {
switch (method) {
case 'API对接':
return <Link className="w-4 h-4" />;
case 'FTP传输':
return <Upload className="w-4 h-4" />;
case 'WebSocket':
return <Wifi className="w-4 h-4" />;
case '手动上传':
return <Upload className="w-4 h-4" />;
default:
return <Database className="w-4 h-4" />;
}
};
const getStatusColor = (status: string) => {
const statusConfig = dataSourceStatuses.find(s => s.key === status);
return statusConfig?.color || '#6b7280';
};
const getTypeColor = (type: string) => {
const typeConfig = dataSourceTypes.find(t => t.key === type);
return typeConfig?.color || '#6b7280';
};
return (
<Card className="p-6 bg-card hover:bg-muted/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ backgroundColor: `${getTypeColor(dataSource.type)}20` }}
>
<Cloud
className="w-5 h-5"
style={{ color: getTypeColor(dataSource.type) }}
/>
</div>
<div>
<h3 className="font-semibold text-foreground">{dataSource.name}</h3>
<p className="text-sm text-muted-foreground">{dataSource.provider}</p>
</div>
</div>
<div className="flex items-center gap-2">
{getStatusIcon(dataSource.status)}
<Badge
variant="outline"
className="font-light"
style={{
borderColor: getStatusColor(dataSource.status),
color: getStatusColor(dataSource.status),
}}
>
{dataSource.status}
</Badge>
</div>
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<Badge
variant="outline"
className="font-light"
style={{
borderColor: getTypeColor(dataSource.type),
color: getTypeColor(dataSource.type),
}}
>
{dataSource.type}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<div className="flex items-center gap-2">
{getAccessMethodIcon(dataSource.accessMethod)}
<span className="text-sm">{dataSource.accessMethod}</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{dataSource.updateFrequency}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium">{dataSource.dataPoints.toLocaleString()}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{dataSource.lastUpdateTime}</span>
</div>
</div>
<div className="mb-4">
<p className="text-sm text-muted-foreground line-clamp-2">
{dataSource.description}
</p>
</div>
<div className="flex flex-wrap gap-1 mb-4">
{dataSource.dataFields.slice(0, 3).map((field, index) => (
<Badge
key={index}
variant="secondary"
className="text-xs font-light"
>
{field}
</Badge>
))}
{dataSource.dataFields.length > 3 && (
<Badge variant="secondary" className="text-xs font-light">
+{dataSource.dataFields.length - 3}
</Badge>
)}
</div>
<div className="flex gap-2 pt-4 border-t border-border">
<Button
variant="outline"
size="sm"
onClick={() => onView(dataSource)}
className="flex-1"
>
<Eye className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onEdit(dataSource)}
className="flex-1"
>
<Edit className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onDelete(dataSource.id)}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</Card>
);
}

View File

@@ -0,0 +1,164 @@
'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { ExternalDataState, ExternalDataAction } from './externalDataReducer';
import { dataSourceTypes, dataSourceStatuses } from '../types';
import { Search, Filter, X } from 'lucide-react';
interface FilterPanelProps {
state: ExternalDataState;
dispatch: React.Dispatch<ExternalDataAction>;
}
export function FilterPanel({ state, dispatch }: FilterPanelProps) {
const [isExpanded, setIsExpanded] = useState(false);
const uniqueProviders = Array.from(new Set(state.dataSources.map(ds => ds.provider)));
const handleFilterChange = (key: keyof ExternalDataState['filters'], value: any) => {
dispatch({ type: 'UPDATE_FILTER', payload: { key, value } });
};
const handleToggleFilter = (filterType: 'type' | 'status' | 'provider', value: string) => {
dispatch({ type: 'TOGGLE_ARRAY_FILTER', payload: { key: filterType, value } });
};
const clearAllFilters = () => {
dispatch({ type: 'CLEAR_FILTERS' });
};
const hasActiveFilters = Object.values(state.filters).some(
value => Array.isArray(value) ? value.length > 0 : value !== ''
);
const activeFilterCount = Object.values(state.filters).reduce(
(count, value) => count + (Array.isArray(value) ? value.length : (value ? 1 : 0)),
0
);
return (
<Card className="bg-card border-border">
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground" />
<h3 className="font-medium"></h3>
{hasActiveFilters && (
<Badge variant="secondary" className="ml-2">
{activeFilterCount}
</Badge>
)}
</div>
<div className="flex items-center gap-2">
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4 mr-1" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? '收起' : '展开'}
</Button>
</div>
</div>
{/* 搜索框 */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索数据源名称、提供商..."
value={state.filters.searchTerm}
onChange={(e) => handleFilterChange('searchTerm', e.target.value)}
className="pl-10"
/>
</div>
{isExpanded && (
<div className="space-y-4">
{/* 数据类型筛选 */}
<div>
<Label className="text-sm font-medium mb-2 block"></Label>
<div className="flex flex-wrap gap-2">
{dataSourceTypes.map((type) => (
<Badge
key={type.key}
variant={state.filters.type.includes(type.key) ? 'default' : 'outline'}
className="cursor-pointer font-light"
style={{
backgroundColor: state.filters.type.includes(type.key) ? type.color : 'transparent',
borderColor: type.color,
color: state.filters.type.includes(type.key) ? 'white' : type.color,
}}
onClick={() => handleToggleFilter('type', type.key)}
>
{type.name}
</Badge>
))}
</div>
</div>
{/* 状态筛选 */}
<div>
<Label className="text-sm font-medium mb-2 block"></Label>
<div className="flex flex-wrap gap-2">
{dataSourceStatuses.map((status) => (
<Badge
key={status.key}
variant={state.filters.status.includes(status.key) ? 'default' : 'outline'}
className="cursor-pointer font-light"
style={{
backgroundColor: state.filters.status.includes(status.key) ? status.color : 'transparent',
borderColor: status.color,
color: state.filters.status.includes(status.key) ? 'white' : status.color,
}}
onClick={() => handleToggleFilter('status', status.key)}
>
{status.name}
</Badge>
))}
</div>
</div>
{/* 提供商筛选 */}
{uniqueProviders.length > 0 && (
<div>
<Label className="text-sm font-medium mb-2 block"></Label>
<div className="flex flex-wrap gap-2">
{uniqueProviders.map((provider) => (
<Badge
key={provider}
variant={state.filters.provider.includes(provider) ? 'default' : 'outline'}
className="cursor-pointer font-light"
style={{
backgroundColor: state.filters.provider.includes(provider) ? '#3b82f6' : 'transparent',
borderColor: '#3b82f6',
color: state.filters.provider.includes(provider) ? 'white' : '#3b82f6',
}}
onClick={() => handleToggleFilter('provider', provider)}
>
{provider}
</Badge>
))}
</div>
</div>
)}
</div>
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,78 @@
'use client';
import { Card } from '@/components/ui/card';
import { ExternalDataState } from './externalDataReducer';
import {
Database,
Cloud,
Activity,
Clock,
TrendingUp,
CheckCircle,
} from 'lucide-react';
interface StatisticsOverviewProps {
state: ExternalDataState;
}
export function StatisticsOverview({ state }: StatisticsOverviewProps) {
const activeRate = state.statistics.totalSources > 0
? (state.statistics.activeSources / state.statistics.totalSources * 100).toFixed(1)
: '0';
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{/* 总数据源 */}
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-blue-600 dark:text-blue-400 font-light"></p>
<p className="text-2xl font-bold text-blue-700 dark:text-blue-300">
{state.statistics.totalSources}
</p>
</div>
<Database className="w-8 h-8 text-blue-500 dark:text-blue-400" />
</div>
</Card>
{/* 活跃数据源 */}
<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>
<p className="text-sm text-green-600 dark:text-green-400 font-light"></p>
<p className="text-2xl font-bold text-green-700 dark:text-green-300">
{state.statistics.activeSources}
</p>
</div>
<CheckCircle className="w-8 h-8 text-green-500 dark:text-green-400" />
</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>
<p className="text-sm text-purple-600 dark:text-purple-400 font-light"></p>
<p className="text-2xl font-bold text-purple-700 dark:text-purple-300">
{state.statistics.totalDataPoints.toLocaleString()}
</p>
</div>
<Cloud className="w-8 h-8 text-purple-500 dark:text-purple-400" />
</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>
<p className="text-sm text-orange-600 dark:text-orange-400 font-light"></p>
<p className="text-2xl font-bold text-orange-700 dark:text-orange-300">
{activeRate}%
</p>
</div>
<TrendingUp className="w-8 h-8 text-orange-500 dark:text-orange-400" />
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,223 @@
'use client';
import { ExternalDataSource, DataSourceType, DataSourceStatus, AccessMethod } from '../types';
export interface ExternalDataState {
dataSources: ExternalDataSource[];
filters: {
type: string[];
status: string[];
provider: string[];
searchTerm: string;
};
selectedDataSource: ExternalDataSource | null;
showAddDialog: boolean;
showUploadDialog: boolean;
showEditDialog: boolean;
showDataPreviewDialog: boolean;
uploadedFile: File | null;
uploadProgress: number;
selectedAccessMethod: AccessMethod;
statistics: {
totalSources: number;
activeSources: number;
totalDataPoints: number;
lastUpdateTime: string;
};
}
export type ExternalDataAction =
| { type: 'SET_DATA_SOURCES'; payload: ExternalDataSource[] }
| { type: 'SET_FILTERS'; payload: Partial<ExternalDataState['filters']> }
| { type: 'UPDATE_FILTER'; payload: { key: keyof ExternalDataState['filters']; value: any } }
| { type: 'TOGGLE_ARRAY_FILTER'; payload: { key: 'type' | 'status' | 'provider'; value: string } }
| { type: 'CLEAR_FILTERS' }
| { type: 'SET_SELECTED_DATA_SOURCE'; payload: ExternalDataSource | null }
| { type: 'SHOW_ADD_DIALOG'; payload: boolean }
| { type: 'SHOW_UPLOAD_DIALOG'; payload: boolean }
| { type: 'SHOW_EDIT_DIALOG'; payload: boolean }
| { type: 'SHOW_DATA_PREVIEW_DIALOG'; payload: boolean }
| { type: 'SET_UPLOADED_FILE'; payload: File | null }
| { type: 'SET_UPLOAD_PROGRESS'; payload: number }
| { type: 'SET_SELECTED_ACCESS_METHOD'; payload: AccessMethod }
| { type: 'ADD_DATA_SOURCE'; payload: ExternalDataSource }
| { type: 'UPDATE_DATA_SOURCE'; payload: { id: string; updates: Partial<ExternalDataSource> } }
| { type: 'DELETE_DATA_SOURCE'; payload: string }
| { type: 'SET_STATISTICS'; payload: Partial<ExternalDataState['statistics']> };
export const initialState: ExternalDataState = {
dataSources: [],
filters: {
type: [],
status: [],
provider: [],
searchTerm: '',
},
selectedDataSource: null,
showAddDialog: false,
showUploadDialog: false,
showEditDialog: false,
showDataPreviewDialog: false,
uploadedFile: null,
uploadProgress: 0,
selectedAccessMethod: 'API对接',
statistics: {
totalSources: 0,
activeSources: 0,
totalDataPoints: 0,
lastUpdateTime: '',
},
};
export function externalDataReducer(state: ExternalDataState, action: ExternalDataAction): ExternalDataState {
switch (action.type) {
case 'SET_DATA_SOURCES':
return {
...state,
dataSources: action.payload,
};
case 'SET_FILTERS':
return {
...state,
filters: {
...state.filters,
...action.payload,
},
};
case 'UPDATE_FILTER':
return {
...state,
filters: {
...state.filters,
[action.payload.key]: action.payload.value,
},
};
case 'TOGGLE_ARRAY_FILTER':
const { key, value } = action.payload;
const currentArray = state.filters[key];
const newArray = currentArray.includes(value)
? currentArray.filter(v => v !== value)
: [...currentArray, value];
return {
...state,
filters: {
...state.filters,
[key]: newArray,
},
};
case 'CLEAR_FILTERS':
return {
...state,
filters: {
type: [],
status: [],
provider: [],
searchTerm: '',
},
};
case 'SET_SELECTED_DATA_SOURCE':
return {
...state,
selectedDataSource: action.payload,
};
case 'SHOW_ADD_DIALOG':
return {
...state,
showAddDialog: action.payload,
};
case 'SHOW_UPLOAD_DIALOG':
return {
...state,
showUploadDialog: action.payload,
};
case 'SHOW_EDIT_DIALOG':
return {
...state,
showEditDialog: action.payload,
};
case 'SHOW_DATA_PREVIEW_DIALOG':
return {
...state,
showDataPreviewDialog: action.payload,
};
case 'SET_UPLOADED_FILE':
return {
...state,
uploadedFile: action.payload,
};
case 'SET_UPLOAD_PROGRESS':
return {
...state,
uploadProgress: action.payload,
};
case 'SET_SELECTED_ACCESS_METHOD':
return {
...state,
selectedAccessMethod: action.payload,
};
case 'ADD_DATA_SOURCE':
return {
...state,
dataSources: [...state.dataSources, action.payload],
};
case 'UPDATE_DATA_SOURCE':
return {
...state,
dataSources: state.dataSources.map(ds =>
ds.id === action.payload.id
? { ...ds, ...action.payload.updates }
: ds
),
};
case 'DELETE_DATA_SOURCE':
return {
...state,
dataSources: state.dataSources.filter(ds => ds.id !== action.payload),
};
case 'SET_STATISTICS':
return {
...state,
statistics: {
...state.statistics,
...action.payload,
},
};
default:
return state;
}
}
export function calculateStatistics(dataSources: ExternalDataSource[]): ExternalDataState['statistics'] {
const totalSources = dataSources.length;
const activeSources = dataSources.filter(ds => ds.status === '正常').length;
const totalDataPoints = dataSources.reduce((sum, ds) => sum + ds.dataPoints, 0);
const lastUpdateTime = dataSources
.filter(ds => ds.status === '正常')
.map(ds => ds.lastUpdateTime)
.sort()
.pop() || '';
return {
totalSources,
activeSources,
totalDataPoints,
lastUpdateTime,
};
}

View File

@@ -0,0 +1,831 @@
/**
* filekorolheader: 外部数据源管理页面 - 多源数据接入管理中心
* 功能:外部数据源管理、数据质量监控、异常告警处理、数据接入配置
* 路径:/ai-crop-model/data-sense-center/external
* 规范遵循crop-x/docs/开发项目规范.md使用useState状态管理shadcn语义化样式
*/
'use client';
import { useState, useEffect } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Progress } from '@/components/ui/progress';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
Database,
Plus,
Upload,
RefreshCw,
CheckCircle,
XCircle,
AlertCircle,
Clock,
Cloud,
Satellite,
Wifi,
WifiOff,
Activity,
Thermometer,
Droplet,
Sun,
Wind,
Camera,
Gauge,
Zap,
Settings,
Eye,
Edit,
Trash2,
Link,
PlayCircle,
PauseCircle,
BarChart3,
Calendar,
MapPin,
Signal,
CheckCircle2,
AlertTriangle,
TrendingUp,
Filter,
FileText,
Search,
Download,
} from 'lucide-react';
import { toast } from 'sonner';
type DataSourceType = '气象数据' | '卫星遥感' | '土壤数据' | '作物生长' | '其他';
type DataSourceStatus = '正常' | '异常' | '离线' | '待配置';
type AccessMethod = '手动上传' | 'API对接' | 'FTP传输' | 'WebSocket';
interface ExternalDataSource {
id: string;
name: string;
type: DataSourceType;
provider: string;
accessMethod: AccessMethod;
apiEndpoint?: string;
updateFrequency: string;
lastUpdateTime: string;
dataPoints: number;
status: DataSourceStatus;
dataFields: string[];
description: string;
}
interface DataQuality {
completeness: number;
accuracy: number;
timeliness: number;
consistency: number;
}
export default function ExternalPage() {
const [activeTab, setActiveTab] = useState('external');
const [showDataSourceDialog, setShowDataSourceDialog] = useState(false);
const [showUploadDialog, setShowUploadDialog] = useState(false);
const [showValidationDialog, setShowValidationDialog] = useState(false);
const [showDataPreviewDialog, setShowDataPreviewDialog] = useState(false);
const [selectedDataSource, setSelectedDataSource] = useState<ExternalDataSource | null>(null);
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const [uploadProgress, setUploadProgress] = useState(0);
const [selectedAccessMethod, setSelectedAccessMethod] = useState<AccessMethod>('API对接');
const [searchTerm, setSearchTerm] = useState('');
const [selectedType, setSelectedType] = useState<string>('all');
const [selectedStatus, setSelectedStatus] = useState<string>('all');
// 外部数据源模拟数据
const [externalDataSources] = useState<ExternalDataSource[]>([
{
id: 'ext-1',
name: '国家气象局API',
type: '气象数据',
provider: '中国气象局',
accessMethod: 'API对接',
apiEndpoint: 'https://api.weather.gov.cn/v1/data',
updateFrequency: '每小时',
lastUpdateTime: '2024-10-15 14:00:00',
dataPoints: 24850,
status: '正常',
dataFields: ['温度', '湿度', '气压', '降水量', '风速', '风向'],
description: '实时气象数据,包含温度、湿度、降水等多维度信息',
},
{
id: 'ext-2',
name: 'Sentinel-2卫星数据',
type: '卫星遥感',
provider: 'ESA欧空局',
accessMethod: 'API对接',
apiEndpoint: 'https://scihub.copernicus.eu/dhus',
updateFrequency: '每5天',
lastUpdateTime: '2024-10-12 08:30:00',
dataPoints: 1280,
status: '正常',
dataFields: ['NDVI', 'EVI', 'LAI', '地表温度', '土壤湿度指数'],
description: '高分辨率卫星遥感影像,用于作物长势监测',
},
{
id: 'ext-3',
name: '土壤数据库',
type: '土壤数据',
provider: '农业部土壤监测中心',
accessMethod: '手动上传',
updateFrequency: '每季度',
lastUpdateTime: '2024-09-20 10:15:00',
dataPoints: 385,
status: '正常',
dataFields: ['pH值', '有机质', '氮磷钾含量', '土壤质地', '盐分'],
description: '区域土壤理化性质数据',
},
{
id: 'ext-4',
name: '光照辐射数据',
type: '气象数据',
provider: '光伏气象站网络',
accessMethod: 'FTP传输',
updateFrequency: '每30分钟',
lastUpdateTime: '2024-10-15 13:30:00',
dataPoints: 15620,
status: '正常',
dataFields: ['总辐射', '直接辐射', '散射辐射', '光合有效辐射'],
description: '太阳辐射数据,用于光合作用分析',
},
{
id: 'ext-5',
name: '作物生长监测',
type: '作物生长',
provider: '农业科学院',
accessMethod: 'API对接',
updateFrequency: '每周',
lastUpdateTime: '2024-10-14 09:00:00',
dataPoints: 2450,
status: '异常',
dataFields: ['株高', '叶面积指数', '生物量', '产量预测'],
description: '作物生长参数动态监测数据',
},
]);
// 数据质量评估
const [dataQuality] = useState<DataQuality>({
completeness: 94.5,
accuracy: 96.8,
timeliness: 92.3,
consistency: 95.2,
});
// 统计数据
const totalExternalSources = externalDataSources.length;
const activeExternalSources = externalDataSources.filter(s => s.status === '正常').length;
// 过滤数据源
const filteredDataSources = externalDataSources.filter(source => {
const matchesSearch = source.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
source.provider.toLowerCase().includes(searchTerm.toLowerCase()) ||
source.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType = selectedType === 'all' || source.type === selectedType;
const matchesStatus = selectedStatus === 'all' || source.status === selectedStatus;
return matchesSearch && matchesType && matchesStatus;
});
const getStatusColor = (status: DataSourceStatus) => {
switch (status) {
case '正常':
return 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/50 dark:text-green-300';
case '异常':
return 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/50 dark:text-red-300';
case '离线':
return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/50 dark:text-gray-300';
case '待配置':
return 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/50 dark:text-yellow-300';
default:
return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/50 dark:text-gray-300';
}
};
const getStatusIcon = (status: DataSourceStatus) => {
switch (status) {
case '正常':
return <CheckCircle className="w-4 h-4 text-green-600" />;
case '异常':
return <XCircle className="w-4 h-4 text-red-600" />;
case '离线':
return <WifiOff className="w-4 h-4 text-gray-500" />;
case '待配置':
return <AlertCircle className="w-4 h-4 text-yellow-600" />;
default:
return <AlertCircle className="w-4 h-4 text-gray-500" />;
}
};
const getDataTypeIcon = (type: DataSourceType) => {
switch (type) {
case '气象数据':
return <Cloud className="w-4 h-4 text-blue-600" />;
case '卫星遥感':
return <Satellite className="w-4 h-4 text-purple-600" />;
case '土壤数据':
return <Database className="w-4 h-4 text-green-600" />;
case '作物生长':
return <TrendingUp className="w-4 h-4 text-orange-600" />;
default:
return <Database className="w-4 h-4 text-gray-600" />;
}
};
const getAccessMethodIcon = (method: string) => {
switch (method) {
case 'API对接':
return <Link className="w-4 h-4" />;
case 'FTP传输':
return <Upload className="w-4 h-4" />;
case 'WebSocket':
return <Wifi className="w-4 h-4" />;
case '手动上传':
return <Upload className="w-4 h-4" />;
default:
return <Database className="w-4 h-4" />;
}
};
const handleTestConnection = () => {
toast.success('连接测试成功,数据接入正常');
};
const handleDataPreview = () => {
setShowDataPreviewDialog(true);
};
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
const fileArray = Array.from(files);
setUploadedFiles(fileArray);
setUploadedFile(fileArray[0]);
let progress = 0;
const interval = setInterval(() => {
progress += 10;
setUploadProgress(progress);
if (progress >= 100) {
clearInterval(interval);
toast.success(`成功上传${fileArray.length}个文件`);
}
}, 200);
}
};
return (
<div className="p-6 space-y-6 bg-background">
{/* 页面标题和说明 */}
<div className="space-y-4">
<div className="flex items-center gap-3">
<Database className="w-8 h-8 text-blue-600" />
<div>
<h1 className="text-3xl font-bold text-foreground"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-sky-600">{activeExternalSources}/{totalExternalSources}</p>
<p className="text-xs text-sky-600 mt-1"></p>
</div>
<Database className="w-12 h-12 text-sky-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-green-600">3/5</p>
<p className="text-xs text-green-600 mt-1">线</p>
</div>
<Wifi className="w-12 h-12 text-green-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-green-600">{dataQuality.accuracy}%</p>
<p className="text-xs text-green-600 mt-1"></p>
</div>
<CheckCircle2 className="w-12 h-12 text-green-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-amber-600">2</p>
<p className="text-xs text-amber-600 mt-1"></p>
</div>
<AlertTriangle className="w-12 h-12 text-amber-600 opacity-50" />
</div>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="external"></TabsTrigger>
<TabsTrigger value="iot"></TabsTrigger>
</TabsList>
{/* 多源数据接入 */}
<TabsContent value="external" className="space-y-4">
<Card className="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200 dark:from-blue-950/50 dark:to-indigo-950/50 dark:border-blue-800">
<div className="flex items-start gap-2">
<Database className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800 dark:text-blue-200">
<p className="mb-2 font-medium"></p>
<ul className="space-y-1 text-xs">
<li> <strong></strong>: API对接FTP传输等多种方式</li>
<li> <strong></strong>: </li>
<li> <strong></strong>: 湿</li>
<li> <strong></strong>: NDVI植被指数</li>
<li> <strong></strong>: </li>
</ul>
</div>
</div>
</Card>
<div className="flex gap-4">
<Button onClick={() => {
setSelectedDataSource(null);
setSelectedAccessMethod('API对接');
setUploadedFile(null);
setUploadedFiles([]);
setUploadProgress(0);
setShowDataSourceDialog(true);
}}>
<Plus className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => setShowUploadDialog(true)}>
<Upload className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => setShowValidationDialog(true)}>
<CheckCircle2 className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 筛选条件 */}
<Card className="p-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Search className="w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索数据源..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-64"
/>
</div>
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-40">
<SelectValue placeholder="数据类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="气象数据"></SelectItem>
<SelectItem value="卫星遥感"></SelectItem>
<SelectItem value="土壤数据"></SelectItem>
<SelectItem value="作物生长"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-32">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="正常"></SelectItem>
<SelectItem value="异常"></SelectItem>
<SelectItem value="离线">线</SelectItem>
<SelectItem value="待配置"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
{/* 数据源列表 */}
<Card className="bg-card border-border">
<div className="p-4 border-b border-border bg-muted/30">
<h3 className="flex items-center gap-2 text-lg font-semibold">
<Database className="w-5 h-5 text-blue-600" />
</h3>
</div>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
<TableHead className="font-medium"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredDataSources.map((source) => (
<TableRow key={source.id} className="hover:bg-muted/30">
<TableCell>
<div className="flex items-center gap-2">
{getDataTypeIcon(source.type)}
<span className="font-medium">{source.name}</span>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="font-light">{source.type}</Badge>
</TableCell>
<TableCell className="text-xs">{source.provider}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getAccessMethodIcon(source.accessMethod)}
<Badge variant="outline" className="font-light">{source.accessMethod}</Badge>
</div>
</TableCell>
<TableCell className="text-xs">{source.updateFrequency}</TableCell>
<TableCell className="text-xs">{source.dataPoints.toLocaleString()}</TableCell>
<TableCell className="text-xs">{source.lastUpdateTime}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getStatusIcon(source.status)}
<Badge className={`font-light ${getStatusColor(source.status)}`}>{source.status}</Badge>
</div>
</TableCell>
<TableCell>
<div className="flex gap-2">
{source.accessMethod !== '手动上传' && (
<Button size="sm" variant="outline" onClick={handleTestConnection}>
<Link className="w-3 h-3" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => {
setSelectedDataSource(source);
setSelectedAccessMethod(source.accessMethod);
setShowDataSourceDialog(true);
}}>
<Edit className="w-3 h-3" />
</Button>
<Button size="sm" variant="outline" onClick={handleDataPreview}>
<Eye className="w-3 h-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Card>
</TabsContent>
</Tabs>
{/* 数据源配置对话框 */}
<Dialog open={showDataSourceDialog} onOpenChange={setShowDataSourceDialog}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Database className="w-5 h-5" />
{selectedDataSource ? '编辑数据源' : '添加数据源'}
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input placeholder="输入数据源名称" defaultValue={selectedDataSource?.name} />
</div>
<div>
<Label></Label>
<Select defaultValue={selectedDataSource?.type}>
<SelectTrigger>
<SelectValue placeholder="选择数据类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="气象数据"></SelectItem>
<SelectItem value="卫星遥感"></SelectItem>
<SelectItem value="土壤数据"></SelectItem>
<SelectItem value="作物生长"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input placeholder="输入数据提供商" defaultValue={selectedDataSource?.provider} />
</div>
<div>
<Label></Label>
<Select value={selectedAccessMethod} onValueChange={(value: AccessMethod) => setSelectedAccessMethod(value)}>
<SelectTrigger>
<SelectValue placeholder="选择接入方式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="API对接">API对接</SelectItem>
<SelectItem value="FTP传输">FTP传输</SelectItem>
<SelectItem value="WebSocket">WebSocket</SelectItem>
<SelectItem value="手动上传"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{selectedAccessMethod !== '手动上传' && (
<div>
<Label>API端点/</Label>
<Input placeholder="输入API端点或服务器地址" defaultValue={selectedDataSource?.apiEndpoint} />
</div>
)}
<div>
<Label></Label>
<Select defaultValue={selectedDataSource?.updateFrequency}>
<SelectTrigger>
<SelectValue placeholder="选择更新频率" />
</SelectTrigger>
<SelectContent>
<SelectItem value="实时"></SelectItem>
<SelectItem value="每5分钟">5</SelectItem>
<SelectItem value="每30分钟">30</SelectItem>
<SelectItem value="每小时"></SelectItem>
<SelectItem value="每天"></SelectItem>
<SelectItem value="每周"></SelectItem>
<SelectItem value="每月"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Textarea
placeholder="输入数据源的详细描述"
className="resize-none"
rows={3}
defaultValue={selectedDataSource?.description}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowDataSourceDialog(false)}>
</Button>
<Button onClick={() => {
toast.success(selectedDataSource ? '数据源更新成功' : '数据源添加成功');
setShowDataSourceDialog(false);
}}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 手动上传数据对话框 */}
<Dialog open={showUploadDialog} onOpenChange={setShowUploadDialog}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Upload className="w-5 h-5 text-blue-600" />
</DialogTitle>
<DialogDescription>
CSVJSONXML等多种格式数据上传
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label></Label>
<Input
type="file"
multiple
accept=".csv,.json,.xml,.xlsx"
onChange={handleFileUpload}
/>
</div>
{uploadProgress > 0 && (
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm"></span>
<span className="text-sm font-medium">{uploadProgress}%</span>
</div>
<Progress value={uploadProgress} className="h-2" />
</div>
)}
<Card className="p-4 bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800">
<h4 className="text-sm mb-3 flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-blue-600" />
</h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs"></span>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<span className="text-xs"></span>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<span className="text-xs"></span>
<Switch defaultChecked />
</div>
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowUploadDialog(false)}>
</Button>
<Button onClick={() => {
toast.success('数据上传成功');
setShowUploadDialog(false);
}}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 数据校验规则对话框 */}
<Dialog open={showValidationDialog} onOpenChange={setShowValidationDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5" />
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm"></Label>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<Label className="text-sm"></Label>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<Label className="text-sm"></Label>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<Label className="text-sm"></Label>
<Switch defaultChecked />
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowValidationDialog(false)}>
</Button>
<Button onClick={() => {
toast.success('校验规则保存成功');
setShowValidationDialog(false);
}}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 数据预览对话框 */}
<Dialog open={showDataPreviewDialog} onOpenChange={setShowDataPreviewDialog}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Eye className="w-5 h-5" />
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{selectedDataSource && (
<>
<Card className="p-4">
<h4 className="font-medium mb-3">{selectedDataSource.name}</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground"></span>
<span className="ml-2">{selectedDataSource.type}</span>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="ml-2">{selectedDataSource.provider}</span>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="ml-2">{selectedDataSource.accessMethod}</span>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="ml-2">{selectedDataSource.updateFrequency}</span>
</div>
</div>
<div className="mt-3">
<span className="text-muted-foreground"></span>
<p className="mt-1 text-sm">{selectedDataSource.description}</p>
</div>
</Card>
<Card className="p-4">
<h4 className="font-medium mb-3"></h4>
<div className="flex flex-wrap gap-2">
{selectedDataSource.dataFields.map((field, index) => (
<Badge key={index} variant="outline">{field}</Badge>
))}
</div>
</Card>
<Card className="p-4">
<h4 className="font-medium mb-3"></h4>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-muted-foreground"></span>
<span className="ml-2 font-medium">{selectedDataSource.dataPoints.toLocaleString()}</span>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="ml-2">{selectedDataSource.lastUpdateTime}</span>
</div>
<div>
<span className="text-muted-foreground"></span>
<Badge className={`ml-2 ${getStatusColor(selectedDataSource.status)}`}>
{selectedDataSource.status}
</Badge>
</div>
</div>
</Card>
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowDataPreviewDialog(false)}>
</Button>
<Button>
<Download className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,115 @@
export type DataSourceType = '气象数据' | '卫星遥感' | '土壤数据' | '作物生长' | '其他';
export type DataSourceStatus = '正常' | '异常' | '离线' | '待配置';
export type AccessMethod = '手动上传' | 'API对接' | 'FTP传输' | 'WebSocket';
export interface ExternalDataSource {
id: string;
name: string;
type: DataSourceType;
provider: string;
accessMethod: AccessMethod;
apiEndpoint?: string;
updateFrequency: string;
lastUpdateTime: string;
dataPoints: number;
status: DataSourceStatus;
dataFields: string[];
description: string;
}
export interface DataSourceForm {
name: string;
type: DataSourceType;
provider: string;
accessMethod: AccessMethod;
apiEndpoint?: string;
updateFrequency: string;
description: string;
}
export const dataSourceTypes: Array<{ key: DataSourceType; name: string; color: string }> = [
{ key: '气象数据', name: '气象数据', color: '#3b82f6' },
{ key: '卫星遥感', name: '卫星遥感', color: '#10b981' },
{ key: '土壤数据', name: '土壤数据', color: '#8b5cf6' },
{ key: '作物生长', name: '作物生长', color: '#f59e0b' },
{ key: '其他', name: '其他', color: '#6b7280' },
];
export const dataSourceStatuses: Array<{ key: DataSourceStatus; name: string; color: string }> = [
{ key: '正常', name: '正常', color: '#10b981' },
{ key: '异常', name: '异常', color: '#ef4444' },
{ key: '离线', name: '离线', color: '#6b7280' },
{ key: '待配置', name: '待配置', color: '#f59e0b' },
];
export const accessMethods: AccessMethod[] = ['手动上传', 'API对接', 'FTP传输', 'WebSocket'];
export const sampleDataSources: ExternalDataSource[] = [
{
id: 'ext-1',
name: '国家气象局API',
type: '气象数据',
provider: '中国气象局',
accessMethod: 'API对接',
apiEndpoint: 'https://api.weather.gov.cn/v1/data',
updateFrequency: '每小时',
lastUpdateTime: '2024-10-15 14:00:00',
dataPoints: 24850,
status: '正常',
dataFields: ['温度', '湿度', '气压', '降水量', '风速', '风向'],
description: '实时气象数据,包含温度、湿度、降水等多维度信息',
},
{
id: 'ext-2',
name: 'Sentinel-2卫星数据',
type: '卫星遥感',
provider: 'ESA欧空局',
accessMethod: 'API对接',
apiEndpoint: 'https://scihub.copernicus.eu/dhus',
updateFrequency: '每5天',
lastUpdateTime: '2024-10-12 08:30:00',
dataPoints: 1280,
status: '正常',
dataFields: ['NDVI', 'EVI', 'LAI', '地表温度', '土壤湿度指数'],
description: '高分辨率卫星遥感影像,用于作物长势监测',
},
{
id: 'ext-3',
name: '土壤数据库',
type: '土壤数据',
provider: '农业部土壤监测中心',
accessMethod: '手动上传',
updateFrequency: '每季度',
lastUpdateTime: '2024-09-20 10:15:00',
dataPoints: 385,
status: '正常',
dataFields: ['pH值', '有机质', '氮磷钾含量', '土壤质地', '盐分'],
description: '区域土壤理化性质数据',
},
{
id: 'ext-4',
name: '光照辐射数据',
type: '气象数据',
provider: '光伏气象站网络',
accessMethod: 'FTP传输',
updateFrequency: '每30分钟',
lastUpdateTime: '2024-10-15 13:30:00',
dataPoints: 15620,
status: '正常',
dataFields: ['总辐射', '直接辐射', '散射辐射', '光合有效辐射'],
description: '太阳辐射数据,用于光合作用分析',
},
{
id: 'ext-5',
name: '作物生长监测系统',
type: '作物生长',
provider: '农业大学作物研究所',
accessMethod: 'WebSocket',
updateFrequency: '实时',
lastUpdateTime: '2024-10-15 14:05:00',
dataPoints: 8920,
status: '异常',
dataFields: ['株高', '叶面积指数', '生物量', '产量预测', '病虫害指数'],
description: '作物生长实时监测数据,提供生长预测分析',
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DataCenterPage() {
return (
<div className="space-y-6">
</div>
);
}

View File

@@ -0,0 +1,93 @@
import { Badge } from "@/components/ui/badge";
import { Brain, User, Zap } from "lucide-react";
import type { DecisionLevel, DecisionSource, ExecutionMode } from "./types";
interface DecisionLevelBadgeProps {
level: DecisionLevel;
}
export function DecisionLevelBadge({ level }: DecisionLevelBadgeProps) {
const config: Record<DecisionLevel, { label: string; className: string }> = {
critical: {
label: "紧急",
className: "bg-error-muted text-error-muted-foreground border-error",
},
important: {
label: "重要",
className: "bg-warning-muted text-warning-muted-foreground border-warning",
},
normal: {
label: "一般",
className: "bg-info-muted text-info-muted-foreground border-info",
},
suggestion: {
label: "建议",
className: "bg-success-muted text-success-muted-foreground border-success",
},
};
const { label, className } = config[level];
return (
<Badge variant="outline" className={className}>
{label}
</Badge>
);
}
interface DecisionSourceBadgeProps {
source: DecisionSource;
}
export function DecisionSourceBadge({ source }: DecisionSourceBadgeProps) {
const config: Record<DecisionSource, { label: string; className: string; icon: typeof Brain | typeof User }> = {
auto: {
label: "自动生成",
className: "bg-accent text-accent-foreground border-accent",
icon: Brain,
},
manual: {
label: "手动添加",
className: "bg-info-muted text-info-muted-foreground border-info",
icon: User,
},
};
const { label, className, icon: Icon } = config[source];
return (
<Badge variant="outline" className={className}>
<Icon className="w-3 h-3 mr-1" />
{label}
</Badge>
);
}
interface ExecutionModeBadgeProps {
mode: ExecutionMode;
}
export function ExecutionModeBadge({ mode }: ExecutionModeBadgeProps) {
const config: Record<ExecutionMode, { label: string; className: string; icon: typeof Zap | typeof User }> = {
manual: {
label: "手动执行",
className: "bg-info-muted text-info-muted-foreground border-info",
icon: User,
},
auto: {
label: "自动执行",
className: "bg-success-muted text-success-muted-foreground border-success",
icon: Zap,
},
};
const { label, className, icon: Icon } = config[mode];
return (
<Badge variant="outline" className={className}>
<Icon className="w-3 h-3 mr-1" />
{label}
</Badge>
);
}

View File

@@ -0,0 +1,328 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Droplets, Power, PowerOff, Settings, Thermometer, Zap } from "lucide-react";
import type { DecisionFormState, DecisionLevel, ExecutionMode } from "./types";
interface DecisionFormDialogProps {
mode: 'create' | 'edit';
open: boolean;
onOpenChange: (open: boolean) => void;
formState: DecisionFormState;
onFormChange: <K extends keyof DecisionFormState>(key: K, value: DecisionFormState[K]) => void;
onSubmit: () => void;
}
const triggerDeviceOptions = [
{ value: '土壤传感器-01', label: '土壤传感器-01', icon: Droplets },
{ value: '土壤传感器-02', label: '土壤传感器-02', icon: Droplets },
{ value: '土壤传感器-03', label: '土壤传感器-03', icon: Droplets },
{ value: '温度传感器-01', label: '温度传感器-01', icon: Thermometer },
{ value: '温度传感器-02', label: '温度传感器-02', icon: Thermometer },
{ value: '湿度传感器-01', label: '湿度传感器-01' },
{ value: '光照传感器-01', label: '光照传感器-01' },
{ value: 'CO2传感器-01', label: 'CO2传感器-01' },
];
const triggerParameterOptions = [
{ value: '土壤湿度', label: '土壤湿度 (%)' },
{ value: '土壤温度', label: '土壤温度 (℃)' },
{ value: '空气温度', label: '空气温度 (℃)' },
{ value: '空气湿度', label: '空气湿度 (%)' },
{ value: '光照强度', label: '光照强度 (lux)' },
{ value: 'CO2浓度', label: 'CO₂ 浓度 (ppm)' },
{ value: 'EC值', label: 'EC 值 (mS/cm)' },
{ value: 'PH值', label: 'PH 值' },
];
const compareOperatorOptions = [
{ value: '>', label: '大于' },
{ value: '<', label: '小于' },
{ value: '>=', label: '大于等于' },
{ value: '<=', label: '小于等于' },
{ value: '==', label: '等于' },
];
const targetDeviceOptions = [
'水肥机-01',
'水肥机-02',
'灌溉阀门-A1',
'灌溉阀门-B2',
'排风扇-01',
'排风扇-02',
'喷雾器-01',
'喷雾器-02',
'补光灯-01',
'加热器-01',
];
export function DecisionFormDialog({ mode, open, onOpenChange, formState, onFormChange, onSubmit }: DecisionFormDialogProps) {
const dialogTitle = mode === 'create' ? '新建决策' : '编辑决策';
const dialogDescription =
mode === 'create'
? '创建基于设备参数的业务融合决策。'
: '更新当前决策的触发条件、执行动作与详细内容。';
const submitLabel = mode === 'create' ? '保存决策' : '保存修改';
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{dialogTitle}</DialogTitle>
<DialogDescription>{dialogDescription}</DialogDescription>
</DialogHeader>
<div className="space-y-6">
<Card className="p-4">
<h4 className="mb-4 text-sm font-medium"></h4>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
placeholder="例如3号大棚灌溉决策"
value={formState.name}
onChange={(event) => onFormChange('name', event.target.value)}
/>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Label></Label>
<Select
value={formState.level}
onValueChange={(value) => onFormChange('level', value as DecisionLevel)}
>
<SelectTrigger>
<SelectValue placeholder="选择决策级别" />
</SelectTrigger>
<SelectContent>
<SelectItem value="critical"></SelectItem>
<SelectItem value="important"></SelectItem>
<SelectItem value="normal"></SelectItem>
<SelectItem value="suggestion"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label> (%)</Label>
<Input
type="number"
min={0}
max={100}
value={formState.confidence}
onChange={(event) => onFormChange('confidence', Number(event.target.value))}
/>
</div>
</div>
<div>
<Label> *</Label>
<Select
value={formState.executionMode}
onValueChange={(value) => onFormChange('executionMode', value as ExecutionMode)}
>
<SelectTrigger>
<SelectValue placeholder="选择执行模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual"></SelectItem>
<SelectItem value="auto"></SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground mt-1">
{formState.executionMode === 'auto'
? '当触发条件满足时,系统将自动执行设备控制操作。'
: '需要人工点击执行按钮,验证触发条件后再执行。'}
</p>
</div>
</div>
</Card>
<Card className="p-4 bg-warning/10 border-warning/30">
<div className="flex items-center gap-2 mb-4">
<Settings className="w-5 h-5 text-warning" />
<h4 className="text-sm font-medium"></h4>
</div>
<p className="text-sm text-muted-foreground mb-4">
</p>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Label> *</Label>
<Select value={formState.triggerDevice} onValueChange={(value) => onFormChange('triggerDevice', value)}>
<SelectTrigger>
<SelectValue placeholder="请选择设备" />
</SelectTrigger>
<SelectContent>
{triggerDeviceOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className="flex items-center gap-2">
{option.icon ? <option.icon className="w-4 h-4" /> : null}
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label> *</Label>
<Select
value={formState.triggerParameter}
onValueChange={(value) => onFormChange('triggerParameter', value)}
>
<SelectTrigger>
<SelectValue placeholder="请选择参数" />
</SelectTrigger>
<SelectContent>
{triggerParameterOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div>
<Label> *</Label>
<Select
value={formState.triggerOperator}
onValueChange={(value) => onFormChange('triggerOperator', value)}
>
<SelectTrigger>
<SelectValue placeholder="请选择比较符号" />
</SelectTrigger>
<SelectContent>
{compareOperatorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.value} {option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="md:col-span-2">
<Label> *</Label>
<Input
placeholder="请输入阈值"
value={formState.triggerValue}
onChange={(event) => onFormChange('triggerValue', event.target.value)}
/>
</div>
</div>
</div>
</Card>
<Card className="p-4 bg-success/10 border-success/30">
<div className="flex items-center gap-2 mb-4">
<Zap className="w-5 h-5 text-success" />
<h4 className="text-sm font-medium"></h4>
</div>
<p className="text-sm text-muted-foreground mb-4"></p>
<div className="space-y-4">
<div>
<Label> *</Label>
<Select value={formState.targetDevice} onValueChange={(value) => onFormChange('targetDevice', value)}>
<SelectTrigger>
<SelectValue placeholder="请选择目标设备" />
</SelectTrigger>
<SelectContent>
{targetDeviceOptions.map((device) => (
<SelectItem key={device} value={device}>
{device}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Label> *</Label>
<Select
value={formState.targetAction}
onValueChange={(value) => onFormChange('targetAction', value === 'open' ? 'open' : 'close')}
>
<SelectTrigger>
<SelectValue placeholder="请选择动作" />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">
<span className="flex items-center gap-2">
<Power className="w-4 h-4 text-success" />
</span>
</SelectItem>
<SelectItem value="close">
<span className="flex items-center gap-2">
<PowerOff className="w-4 h-4 text-destructive" />
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>*</Label>
<Input
type="number"
min={1}
value={formState.duration}
onChange={(event) => onFormChange('duration', Number(event.target.value))}
/>
</div>
</div>
</div>
</Card>
<Card className="p-4">
<h4 className="mb-4 text-sm font-medium"></h4>
<div className="space-y-4">
<div>
<Label> *</Label>
<Textarea
rows={3}
value={formState.recommendation}
onChange={(event) => onFormChange('recommendation', event.target.value)}
/>
</div>
<div>
<Label></Label>
<Textarea
rows={5}
value={formState.explanation}
onChange={(event) => onFormChange('explanation', event.target.value)}
/>
</div>
<div>
<Label> *</Label>
<Textarea
rows={6}
value={formState.actionItems}
onChange={(event) => onFormChange('actionItems', event.target.value)}
/>
</div>
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button className="bg-success hover:bg-success/90" onClick={onSubmit}>
{submitLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,48 @@
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { BookOpen, Brain, Gauge, ListChecks, Merge, Plus } from "lucide-react";
interface DecisionFusionHeaderProps {
onCreate: () => void;
}
export function DecisionFusionHeader({ onCreate }: DecisionFusionHeaderProps) {
return (
<Card className="p-6 bg-gradient-to-r from-accent/10 via-primary/10 to-accent/5 border border-accent/30">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
<Merge className="w-6 h-6 text-primary flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="mb-2 text-xl font-semibold"></h2>
<p className="text-sm text-muted-foreground mb-3">
AI模型输出
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="bg-white text-primary border-primary/40">
<Brain className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white text-primary border-primary/40">
<ListChecks className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white text-primary border-primary/40">
<BookOpen className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white text-primary border-primary/40">
<Gauge className="w-3 h-3 mr-1" />
</Badge>
</div>
</div>
</div>
<Button onClick={onCreate} className="bg-success hover:bg-success/90">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
);
}

View File

@@ -0,0 +1,181 @@
import { useMemo } from "react";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Clock,
Edit,
Eye,
Gauge,
Lightbulb,
Play,
Power,
PowerOff,
Settings,
Timer,
Trash2,
User,
Zap,
} from "lucide-react";
import type { DecisionResult } from "./types";
import { DecisionLevelBadge, DecisionSourceBadge, ExecutionModeBadge } from "./DecisionBadges";
interface DecisionListCardProps {
decisions: DecisionResult[];
activeTab: 'all' | 'auto' | 'manual';
onTabChange: (tab: 'all' | 'auto' | 'manual') => void;
onViewDetail: (decision: DecisionResult) => void;
onExecute: (decision: DecisionResult) => void;
onEdit: (decision: DecisionResult) => void;
onDelete: (id: string) => void;
}
export function DecisionListCard({
decisions,
activeTab,
onTabChange,
onViewDetail,
onExecute,
onEdit,
onDelete,
}: DecisionListCardProps) {
const filteredResults = useMemo(() => {
if (activeTab === 'all') return decisions;
if (activeTab === 'auto') return decisions.filter((item) => item.source === 'auto');
return decisions.filter((item) => item.source === 'manual');
}, [activeTab, decisions]);
return (
<Card className="p-6">
<Tabs value={activeTab} onValueChange={(value) => onTabChange(value as 'all' | 'auto' | 'manual')}>
<div className="flex items-center justify-between mb-6">
<TabsList>
<TabsTrigger value="all">
<Badge variant="secondary" className="ml-2">
{decisions.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="auto">
<Badge variant="secondary" className="ml-2">
{decisions.filter((item) => item.source === 'auto').length}
</Badge>
</TabsTrigger>
<TabsTrigger value="manual">
<Badge variant="secondary" className="ml-2">
{decisions.filter((item) => item.source === 'manual').length}
</Badge>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value={activeTab} className="mt-0">
{filteredResults.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<Lightbulb className="w-12 h-12 mx-auto mb-3 opacity-50" />
<div></div>
</div>
) : (
<div className="space-y-4">
{filteredResults.map((result) => (
<Card key={result.id} className="p-5 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2 flex-wrap">
<DecisionSourceBadge source={result.source} />
<ExecutionModeBadge mode={result.executionMode} />
<DecisionLevelBadge level={result.level} />
</div>
<h3 className="mb-2 text-lg font-medium">{result.name}</h3>
<p className="text-sm text-muted-foreground">{result.recommendation}</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 mb-4 p-4 bg-muted rounded-lg">
<div>
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
<Settings className="w-3 h-3" />
</div>
<div className="text-sm leading-relaxed">
<strong>{result.triggerCondition.device}</strong>
<strong className="mx-1">{result.triggerCondition.parameter}</strong>
<strong className="mx-1">{result.triggerCondition.operator}</strong>
<strong>{result.triggerCondition.value}</strong>
</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
<Zap className="w-3 h-3" />
</div>
<div className="text-sm flex items-center gap-2">
{result.execution.action === 'open' ? (
<Power className="w-4 h-4 text-success" />
) : (
<PowerOff className="w-4 h-4 text-error" />
)}
<strong>{result.execution.action === 'open' ? '打开' : '关闭'}</strong>
<strong>{result.execution.device}</strong>
<Timer className="w-3 h-3 ml-2 text-info" />
<span>{result.execution.duration} </span>
</div>
</div>
</div>
<div className="flex flex-wrap items-center justify-between text-sm text-muted-foreground mb-4 gap-3">
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-1">
<Gauge className="w-4 h-4" />
<span> {(result.confidence * 100).toFixed(0)}%</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{result.generatedAt}</span>
</div>
{result.createdBy && (
<div className="flex items-center gap-1">
<User className="w-4 h-4" />
<span>{result.createdBy}</span>
</div>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button size="sm" variant="outline" onClick={() => onViewDetail(result)}>
<Eye className="w-4 h-4 mr-1" />
</Button>
<Button size="sm" onClick={() => onExecute(result)} className="bg-success hover:bg-success/90">
<Play className="w-4 h-4 mr-1" />
{result.executionMode === 'auto' ? '立即执行' : '手动执行'}
</Button>
<Button size="sm" variant="outline" onClick={() => onEdit(result)}>
<Edit className="w-4 h-4 mr-1" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => onDelete(result.id)}
className="text-destructive hover:text-destructive hover:border-destructive/30"
>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
</div>
</Card>
))}
</div>
)}
</TabsContent>
</Tabs>
</Card>
);
}

View File

@@ -0,0 +1,215 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { CheckCircle, Gauge, ListChecks, Power, PowerOff, Settings, Timer, Zap } from "lucide-react";
import type { DecisionResult } from "./types";
import { DecisionLevelBadge, DecisionSourceBadge, ExecutionModeBadge } from "./DecisionBadges";
interface DecisionResultDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
decision: DecisionResult | null;
onExecute: (decision: DecisionResult) => void;
}
export function DecisionResultDialog({ open, onOpenChange, decision, onExecute }: DecisionResultDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{decision && (
<div className="space-y-6">
<div>
<div className="flex items-center gap-2 mb-3">
<DecisionSourceBadge source={decision.source} />
<ExecutionModeBadge mode={decision.executionMode} />
<DecisionLevelBadge level={decision.level} />
</div>
<h3 className="text-lg font-semibold">{decision.name}</h3>
</div>
<Card className="p-4 bg-info/10 border-info/30">
<h4 className="mb-3 text-sm font-medium"></h4>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="p-3 bg-white rounded border border-muted/40">
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
<Settings className="w-3 h-3" />
</div>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>{decision.triggerCondition.device}</strong>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>{decision.triggerCondition.parameter}</strong>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>
{decision.triggerCondition.operator} {decision.triggerCondition.value}
</strong>
</div>
</div>
</div>
<div className="p-3 bg-white rounded border border-muted/40">
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
<Zap className="w-3 h-3" />
</div>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>{decision.execution.device}</strong>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground"></span>
<strong className={decision.execution.action === 'open' ? 'text-success' : 'text-destructive'}>
<span className="inline-flex items-center gap-1">
{decision.execution.action === 'open' ? (
<Power className="w-4 h-4" />
) : (
<PowerOff className="w-4 h-4" />
)}
{decision.execution.action === 'open' ? '打开' : '关闭'}
</span>
</strong>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong className="inline-flex items-center gap-1">
<Timer className="w-3 h-3 text-info" />
{decision.execution.duration}
</strong>
</div>
</div>
</div>
</div>
</Card>
<div>
<h4 className="mb-2 text-sm font-medium"></h4>
<div className="field-value whitespace-pre-wrap leading-relaxed">{decision.recommendation}</div>
</div>
{decision.explanation && (
<div>
<h4 className="mb-2 text-sm font-medium"></h4>
<div className="field-value whitespace-pre-wrap leading-relaxed">{decision.explanation}</div>
</div>
)}
<div>
<h4 className="mb-2 text-sm font-medium"></h4>
<div className="field-value">
<ol className="list-decimal list-inside space-y-1">
{decision.actionItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ol>
</div>
</div>
{decision.fusionProcess.length > 0 && (
<div>
<h4 className="mb-2 text-sm font-medium"></h4>
<div className="space-y-2">
{decision.fusionProcess.map((step, index) => (
<div key={index} className="flex items-start gap-3 p-3 bg-muted rounded">
<div className="w-8 h-8 rounded-full bg-info-muted text-info flex items-center justify-center">
{index + 1}
</div>
<div className="flex-1">
<div className="font-medium">{step.step}</div>
<div className="text-sm text-muted-foreground">{step.result}</div>
<Badge variant="outline" className="mt-1 bg-white border-info/40 text-info">
{step.weight}
</Badge>
</div>
</div>
))}
</div>
</div>
)}
{(decision.inputData.models.length > 0 ||
decision.inputData.rules.length > 0 ||
decision.inputData.context.length > 0) && (
<div>
<h4 className="mb-2 text-sm font-medium"></h4>
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
{decision.inputData.models.length > 0 && (
<div className="p-3 bg-info/10 rounded">
<div className="text-xs text-muted-foreground mb-2"></div>
<div className="space-y-1 text-sm">
{decision.inputData.models.map((model) => (
<div key={model.id}>{model.name}</div>
))}
</div>
</div>
)}
{decision.inputData.rules.length > 0 && (
<div className="p-3 bg-success/10 rounded">
<div className="text-xs text-muted-foreground mb-2"></div>
<div className="space-y-1 text-sm">
{decision.inputData.rules.map((rule) => (
<div key={rule.id}>{rule.name}</div>
))}
</div>
</div>
)}
{decision.inputData.context.length > 0 && (
<div className="p-3 bg-warning/10 rounded">
<div className="text-xs text-muted-foreground mb-2"></div>
<div className="space-y-1 text-sm">
{decision.inputData.context.map((ctx) => (
<div key={ctx.id}>{ctx.name}</div>
))}
</div>
</div>
)}
</div>
</div>
)}
<div className="grid grid-cols-1 gap-4 text-sm md:grid-cols-3">
<div>
<div className="text-muted-foreground mb-1"></div>
<div className="font-medium">{(decision.confidence * 100).toFixed(0)}%</div>
</div>
<div>
<div className="text-muted-foreground mb-1"></div>
<div className="font-medium">{decision.generatedAt}</div>
</div>
{decision.createdBy && (
<div>
<div className="text-muted-foreground mb-1"></div>
<div className="font-medium">{decision.createdBy}</div>
</div>
)}
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
{decision && (
<Button className="bg-success hover:bg-success/90" onClick={() => onExecute(decision)}>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,38 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertCircle } from "lucide-react";
interface DeleteConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
}
export function DeleteConfirmDialog({ open, onOpenChange, onConfirm }: DeleteConfirmDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="flex items-center gap-3 p-4 bg-destructive/10 rounded border border-destructive/30 text-sm text-destructive">
<AlertCircle className="w-5 h-5" />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={onConfirm} className="bg-destructive hover:bg-destructive/90">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,108 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { AlertCircle, Clock, Gauge, Power, PowerOff, Settings, Zap } from "lucide-react";
import type { DecisionResult } from "./types";
interface ExecuteConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
decision: DecisionResult | null;
onConfirm: () => void;
}
export function ExecuteConfirmDialog({ open, onOpenChange, decision, onConfirm }: ExecuteConfirmDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{decision && (
<div className="space-y-4">
<div>
<Label></Label>
<div className="field-value">{decision.name}</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="p-3 bg-info/10 rounded">
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
<Settings className="w-3 h-3" />
</div>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>{decision.triggerCondition.device}</strong>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>{decision.triggerCondition.parameter}</strong>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>
{decision.triggerCondition.operator} {decision.triggerCondition.value}
</strong>
</div>
</div>
</div>
<div className="p-3 bg-success/10 rounded">
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
<Zap className="w-3 h-3" />
</div>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong>{decision.execution.device}</strong>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground"></span>
<strong className={decision.execution.action === 'open' ? 'text-success' : 'text-destructive'}>
<span className="inline-flex items-center gap-1">
{decision.execution.action === 'open' ? (
<Power className="w-4 h-4" />
) : (
<PowerOff className="w-4 h-4" />
)}
{decision.execution.action === 'open' ? '打开' : '关闭'}
</span>
</strong>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<strong className="inline-flex items-center gap-1">
<Clock className="w-3 h-3" />
{decision.execution.duration}
</strong>
</div>
</div>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-warning/10 rounded border border-warning/30 text-sm">
<AlertCircle className="w-4 h-4 text-warning mt-0.5" />
<div>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={onConfirm} className="bg-success hover:bg-success/90">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,97 @@
import type { ComponentType } from "react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { AlertCircle, CheckCircle, Clock, Info, XCircle } from "lucide-react";
import type { ExecuteResult, ExecuteResultDetail, ExecuteResultStatus } from "./types";
interface ExecuteResultDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
result: ExecuteResult | null;
}
type StatusConfig = {
icon: ComponentType<{ className?: string }>;
className: string;
label: string;
};
const statusConfig: Record<ExecuteResultStatus, StatusConfig> = {
success: {
icon: CheckCircle,
className: "text-success",
label: "成功",
},
warning: {
icon: AlertCircle,
className: "text-warning",
label: "警告",
},
error: {
icon: XCircle,
className: "text-destructive",
label: "失败",
},
info: {
icon: Info,
className: "text-info",
label: "信息",
},
};
export function ExecuteResultDialog({ open, onOpenChange, result }: ExecuteResultDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{result && (
<div className="space-y-4">
<Card className={`p-4 ${result.success ? 'bg-success/10 border-success/30' : 'bg-destructive/10 border-destructive/30'}`}>
<div className="flex items-center gap-2">
<CheckCircle className={`w-5 h-5 ${result.success ? 'text-success' : 'text-destructive'}`} />
<div>
<div className="font-medium">{result.success ? '执行成功' : '执行失败'}</div>
<div className="text-sm text-muted-foreground flex items-center gap-2">
<Clock className="w-3 h-3" />
{result.executedAt}
</div>
</div>
</div>
</Card>
<div className="space-y-2">
{result.details.map((detail: ExecuteResultDetail, index) => {
const config = statusConfig[detail.status] ?? statusConfig.info;
const Icon = config.icon;
return (
<div key={index} className="flex items-start gap-3 p-3 bg-muted rounded">
<Icon className={`w-4 h-4 mt-1 ${config.className}`} />
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{detail.step}</span>
<span className={`text-xs ${config.className}`}>{config.label}</span>
</div>
<div className="text-sm text-muted-foreground">{detail.message}</div>
</div>
</div>
);
})}
</div>
</div>
)}
<DialogFooter>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,193 @@
import type { DecisionFormState, DecisionResult } from "./types";
export const initialDecisionResults: DecisionResult[] = [
{
id: "decision_1",
name: "3号大棚灌溉决策",
source: "auto",
executionMode: "auto",
triggerCondition: {
device: "土壤传感器-03",
parameter: "土壤湿度",
operator: "<",
value: "30",
},
execution: {
device: "水肥机-01",
action: "open",
duration: 45,
},
level: "important",
confidence: 0.89,
recommendation: "当土壤湿度低于30%时自动打开水肥机进行灌溉45分钟",
explanation:
"综合分析:\n1. 模型预测土壤湿度为35%,低于开花期最佳湿度(60-70%)\n2. 天气预报未来3天无降雨蒸发量较大(8.5mm/day)\n3. 当前处于开花期,是需水关键期\n4. 历史记录显示上次灌溉已过3天\n因此建议尽快灌溉保证作物正常生长",
actionItems: [
"系统检测到土壤湿度低于30%",
"自动启动水肥机-01",
"持续灌溉45分钟",
"灌溉结束后系统自动关闭",
"记录灌溉时间和用水量",
],
inputData: {
models: [
{ id: "output_3", name: "灌溉需求预测模型" },
{ id: "output_1", name: "番茄生长预测模型" },
],
rules: [
{ id: "rule_2", name: "开花期灌溉规则" },
{ id: "rule_5", name: "干旱预警规则" },
],
context: [
{ id: "ctx_4", name: "天气预报" },
{ id: "ctx_5", name: "历史灌溉记录" },
],
},
fusionProcess: [
{
step: "模型输出分析",
result: "灌溉需求预测: 120升置信度 0.91",
weight: 0.6,
},
{
step: "业务规则匹配",
result: "匹配到“开花期灌溉规则”,权重 0.9",
weight: 0.5,
},
{
step: "上下文验证",
result: "天气晴朗无雨土壤湿度35%,符合灌溉条件",
weight: 0.45,
},
{
step: "加权融合计算",
result: "综合置信度 0.89(超过阈值 0.75",
weight: 1,
},
{
step: "决策生成",
result: "生成可执行灌溉方案",
weight: 1,
},
],
generatedAt: "2024-10-23 10:35:00",
},
{
id: "decision_2",
name: "2号大棚温度控制决策",
source: "manual",
executionMode: "manual",
triggerCondition: {
device: "温度传感器-02",
parameter: "空气温度",
operator: ">",
value: "35",
},
execution: {
device: "排风扇-02",
action: "open",
duration: 20,
},
level: "normal",
confidence: 0.86,
recommendation: "当大棚温度高于35℃时手动打开排风扇20分钟进行降温",
explanation: "根据现场观察2号大棚在高温天气容易超过35℃影响作物生长。建议当温度传感器检测到超过35℃时及时打开排风扇降温。",
actionItems: [
"监测温度传感器-02的实时数据",
"温度超过35℃时收到系统提醒",
"点击执行按钮,启动排风扇-02",
"持续运行20分钟后自动关闭",
"记录降温效果",
],
inputData: {
models: [],
rules: [],
context: [],
},
fusionProcess: [],
generatedAt: "2024-10-22 14:20:00",
createdBy: "张三",
},
{
id: "decision_3",
name: "1号大棚湿度调节决策",
source: "auto",
executionMode: "auto",
triggerCondition: {
device: "湿度传感器-01",
parameter: "空气湿度",
operator: "<",
value: "60",
},
execution: {
device: "喷雾器-01",
action: "open",
duration: 15,
},
level: "suggestion",
confidence: 0.92,
recommendation: "当空气湿度低于60%时自动打开喷雾器15分钟增加湿度",
explanation:
"综合分析:\n1. 番茄生长最佳湿度为60-80%\n2. 当前处于开花期,湿度过低会影响授粉\n3. 设置自动触发条件,保持适宜湿度\n因此建议当湿度低于60%时自动喷雾加湿",
actionItems: [
"系统检测到空气湿度低于60%",
"自动启动喷雾器-01",
"持续喷雾15分钟",
"喷雾结束后系统自动关闭",
"记录湿度变化曲线",
],
inputData: {
models: [{ id: "output_1", name: "番茄生长预测模型" }],
rules: [{ id: "rule_3", name: "开花期湿度规则" }],
context: [
{ id: "ctx_1", name: "地块信息" },
{ id: "ctx_2", name: "作物品种" },
],
},
fusionProcess: [
{
step: "模型输出分析",
result: "最佳湿度范围60-80%",
weight: 0.7,
},
{
step: "业务规则匹配",
result: "匹配到“开花期湿度规则”",
weight: 0.55,
},
{
step: "上下文验证",
result: "当前处于开花期,湿度控制至关重要",
weight: 0.4,
},
{
step: "加权融合计算",
result: "综合置信度 0.92",
weight: 1,
},
{
step: "决策生成",
result: "生成自动湿度控制方案",
weight: 1,
},
],
generatedAt: "2024-10-23 09:15:00",
},
];
export const createDefaultDecisionFormState = (): DecisionFormState => ({
name: "",
level: "normal",
confidence: 80,
executionMode: "manual",
triggerDevice: "",
triggerParameter: "",
triggerOperator: "<",
triggerValue: "",
targetDevice: "",
targetAction: "open",
duration: 30,
recommendation: "",
explanation: "",
actionItems: "",
});

View File

@@ -0,0 +1,86 @@
export type DecisionLevel = 'critical' | 'important' | 'normal' | 'suggestion';
export type DecisionSource = 'auto' | 'manual';
export type ExecutionMode = 'manual' | 'auto';
export type DecisionAction = 'open' | 'close';
export interface TriggerCondition {
device: string;
parameter: string;
operator: string;
value: string;
}
export interface ExecutionSetting {
device: string;
action: DecisionAction;
duration: number;
}
export interface FusionProcessStep {
step: string;
result: string;
weight: number;
}
export interface DecisionInputItem {
id: string;
name: string;
}
export interface DecisionInputData {
models: DecisionInputItem[];
rules: DecisionInputItem[];
context: DecisionInputItem[];
}
export interface DecisionResult {
id: string;
name: string;
source: DecisionSource;
executionMode: ExecutionMode;
triggerCondition: TriggerCondition;
execution: ExecutionSetting;
level: DecisionLevel;
confidence: number;
recommendation: string;
explanation: string;
actionItems: string[];
inputData: DecisionInputData;
fusionProcess: FusionProcessStep[];
generatedAt: string;
createdBy?: string;
}
export interface DecisionFormState {
name: string;
level: DecisionLevel;
confidence: number;
executionMode: ExecutionMode;
triggerDevice: string;
triggerParameter: string;
triggerOperator: string;
triggerValue: string;
targetDevice: string;
targetAction: DecisionAction;
duration: number;
recommendation: string;
explanation: string;
actionItems: string;
}
export type ExecuteResultStatus = 'success' | 'warning' | 'error' | 'info';
export interface ExecuteResultDetail {
step: string;
status: ExecuteResultStatus;
message: string;
}
export interface ExecuteResult {
success: boolean;
executedAt: string;
details: ExecuteResultDetail[];
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DecisionPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/decision
</p>
</div>
</Card>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function KnowledgeGenerationPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/knowledge/generation
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function KnowledgePage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold">AI知识库</h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/knowledge
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function AiCropModelLayout({
children,
}: {
children: React.ReactNode
}) {
return <>{children}</>
}

View File

@@ -0,0 +1,517 @@
/**
* filekorolheader: 应用编辑对话框 - 模型应用编辑流程对话框
* 功能:多步骤应用编辑流程、表单验证、应用更新
* 路径:/ai-crop-model/model-application/generation/components/ApplicationEditDialog
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { ApplicationGenerationState, ApplicationGenerationAction } from './ApplicationGenerationReducer';
import { Application, ApplicationType, OutputFormat } from '@/types/ai-model';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import {
Edit,
ChevronLeft,
ChevronRight,
FileText,
Server,
BarChart3,
LineChart as LineChartIcon,
PieChart as PieChartIcon,
Table as TableIcon,
Type,
CheckCircle,
AlertTriangle,
} from 'lucide-react';
import { toast } from 'sonner';
interface ApplicationEditDialogProps {
state: ApplicationGenerationState;
dispatch: React.Dispatch<ApplicationGenerationAction>;
}
export default function ApplicationEditDialog({ state, dispatch }: ApplicationEditDialogProps) {
const { showEditDialog, editStep, editAppData, availableModels, inputFieldOptions, editingApp } = state;
const handleNextStep = () => {
if (editStep === 1) {
if (!editAppData.name || !editAppData.type || !editAppData.description) {
toast.error('请填写完整的基本信息');
return;
}
} else if (editStep === 2) {
if (!editAppData.modelName || !editAppData.modelVersion) {
toast.error('请选择模型');
return;
}
} else if (editStep === 3) {
if (editAppData.inputFields.length === 0) {
toast.error('请选择至少一个输入字段');
return;
}
} else if (editStep === 4) {
if (!editAppData.outputFormat) {
toast.error('请选择输出格式');
return;
}
}
dispatch({ type: 'SET_EDIT_STEP', payload: editStep + 1 });
};
const handlePrevStep = () => {
dispatch({ type: 'SET_EDIT_STEP', payload: editStep - 1 });
};
const handleSaveEdit = () => {
if (!editingApp) return;
const updatedApp: Application = {
...editingApp,
name: editAppData.name,
type: editAppData.type as ApplicationType,
description: editAppData.description,
modelName: editAppData.modelName,
modelVersion: editAppData.modelVersion,
inputConfig: {
fields: editAppData.inputFields,
},
outputConfig: {
format: editAppData.outputFormat as OutputFormat,
},
};
dispatch({
type: 'UPDATE_APPLICATION',
payload: { id: editingApp.id, updates: updatedApp }
});
dispatch({ type: 'SET_SHOW_EDIT_DIALOG', payload: false });
dispatch({ type: 'SET_EDIT_STEP', payload: 1 });
dispatch({ type: 'SET_EDITING_APP', payload: null });
dispatch({ type: 'RESET_EDIT_APP_DATA' });
toast.success(`应用"${updatedApp.name}"更新成功!`);
};
const handleClose = () => {
dispatch({ type: 'SET_SHOW_EDIT_DIALOG', payload: false });
dispatch({ type: 'SET_EDIT_STEP', payload: 1 });
dispatch({ type: 'SET_EDITING_APP', payload: null });
dispatch({ type: 'RESET_EDIT_APP_DATA' });
};
const getStepIcon = (step: number) => {
switch (step) {
case 1:
return <FileText className="w-4 h-4" />;
case 2:
return <Server className="w-4 h-4" />;
case 3:
return <FileText className="w-4 h-4" />;
case 4:
return <BarChart3 className="w-4 h-4" />;
default:
return <CheckCircle className="w-4 h-4" />;
}
};
const getStepTitle = (step: number) => {
switch (step) {
case 1:
return '填写基本信息';
case 2:
return '选择模型';
case 3:
return '配置输入';
case 4:
return '配置输出';
case 5:
return '预览保存';
default:
return '';
}
};
return (
<Dialog open={showEditDialog} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Edit className="w-5 h-5 text-blue-600" />
- {getStepTitle(editStep)}
</DialogTitle>
<DialogDescription>
{editStep} / 5
</DialogDescription>
</DialogHeader>
{/* 步骤指示器 */}
<div className="flex items-center justify-between mb-6">
{[1, 2, 3, 4, 5].map((step) => (
<div key={step} className="flex items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
step <= editStep
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{step < editStep ? <CheckCircle className="w-4 h-4" /> : getStepIcon(step)}
</div>
<span
className={`ml-2 text-sm ${
step <= editStep ? 'text-blue-600 font-medium' : 'text-gray-500'
}`}
>
{getStepTitle(step)}
</span>
{step < 5 && (
<ChevronRight className="w-4 h-4 mx-4 text-gray-400" />
)}
</div>
))}
</div>
{/* 步骤内容 */}
<div className="min-h-[400px]">
{/* 步骤1: 基本信息 */}
{editStep === 1 && (
<div className="space-y-4">
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2 text-blue-900 dark:text-blue-100">
<FileText className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<div>
<Label> *</Label>
<Input
placeholder="请输入应用名称,如:智能灌溉策略生成"
value={editAppData.name}
onChange={(e) => dispatch({ type: 'SET_EDIT_APP_DATA', payload: { name: e.target.value } })}
/>
</div>
<div>
<Label> *</Label>
<Select
value={editAppData.type}
onValueChange={(value) => dispatch({ type: 'SET_EDIT_APP_DATA', payload: { type: value } })}
>
<SelectTrigger>
<SelectValue placeholder="选择应用类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="智能灌溉"></SelectItem>
<SelectItem value="病虫害预警"></SelectItem>
<SelectItem value="施肥推荐"></SelectItem>
<SelectItem value="产量预测"></SelectItem>
<SelectItem value="生长监测"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label> *</Label>
<Textarea
placeholder="请详细描述应用的功能和使用场景"
value={editAppData.description}
onChange={(e) => dispatch({ type: 'SET_EDIT_APP_DATA', payload: { description: e.target.value } })}
rows={4}
/>
</div>
</div>
)}
{/* 步骤2: 选择模型 */}
{editStep === 2 && (
<div className="space-y-4">
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
<div className="flex items-center gap-2 text-purple-900 dark:text-purple-100">
<Server className="w-4 h-4" />
<span className="text-sm">AI模型</span>
</div>
</Card>
<div>
<Label> *</Label>
<div className="grid grid-cols-1 gap-3 mt-2">
{availableModels.map((model) => (
<div
key={model.id}
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
editAppData.modelName === model.name
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
onClick={() =>
dispatch({
type: 'SET_EDIT_APP_DATA',
payload: { modelName: model.name, modelVersion: model.version }
})
}
>
<div className="flex items-center justify-between">
<div>
<div className="font-medium">{model.name}</div>
<div className="text-sm text-muted-foreground">
: {model.version} | : {model.type}
</div>
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
model.status === '运行中' ? 'bg-green-500' : 'bg-red-500'
}`} />
<span className="text-sm">{model.status}</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* 步骤3: 输入配置 */}
{editStep === 3 && (
<div className="space-y-4">
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<div className="flex items-center gap-2 text-green-900 dark:text-green-100">
<FileText className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<div>
<Label> * ()</Label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mt-2 max-h-64 overflow-y-auto p-2 border rounded-lg">
{inputFieldOptions.map((field) => (
<div key={field} className="flex items-center space-x-2">
<Checkbox
id={`edit-${field}`}
checked={editAppData.inputFields.includes(field)}
onCheckedChange={(checked) => {
if (checked) {
dispatch({
type: 'SET_EDIT_APP_DATA',
payload: {
inputFields: [...editAppData.inputFields, field]
}
});
} else {
dispatch({
type: 'SET_EDIT_APP_DATA',
payload: {
inputFields: editAppData.inputFields.filter(f => f !== field)
}
});
}
}}
/>
<Label htmlFor={`edit-${field}`} className="text-sm font-normal cursor-pointer">
{field}
</Label>
</div>
))}
</div>
<div className="mt-2 text-sm text-muted-foreground">
{editAppData.inputFields.length}
</div>
</div>
</div>
)}
{/* 步骤4: 输出格式 */}
{editStep === 4 && (
<div className="space-y-4">
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
<div className="flex items-center gap-2 text-orange-900 dark:text-orange-100">
<BarChart3 className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<div>
<Label> *</Label>
<div className="grid grid-cols-2 gap-3 mt-2">
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
editAppData.outputFormat === '折线图'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
onClick={() => dispatch({ type: 'SET_EDIT_APP_DATA', payload: { outputFormat: '折线图' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<LineChartIcon className="w-5 h-5 text-blue-600" />
</div>
<div>
<div className="font-medium">线</div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
editAppData.outputFormat === '饼状图'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
onClick={() => dispatch({ type: 'SET_EDIT_APP_DATA', payload: { outputFormat: '饼状图' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<PieChartIcon className="w-5 h-5 text-green-600" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
editAppData.outputFormat === '表格'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
onClick={() => dispatch({ type: 'SET_EDIT_APP_DATA', payload: { outputFormat: '表格' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<TableIcon className="w-5 h-5 text-purple-600" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
editAppData.outputFormat === '文字'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
onClick={() => dispatch({ type: 'SET_EDIT_APP_DATA', payload: { outputFormat: '文字' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<Type className="w-5 h-5 text-orange-600" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* 步骤5: 确认保存 */}
{editStep === 5 && (
<div className="space-y-4">
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<div className="flex items-center gap-2 text-green-900 dark:text-green-100">
<CheckCircle className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<Card className="p-6">
<h3 className="font-semibold mb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{editAppData.name}</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{editAppData.type}</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{editAppData.modelName}</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{editAppData.modelVersion}</div>
</div>
<div className="col-span-2">
<Label className="text-sm text-muted-foreground"></Label>
<div className="text-sm">{editAppData.description}</div>
</div>
<div className="col-span-2">
<Label className="text-sm text-muted-foreground"> ({editAppData.inputFields.length})</Label>
<div className="flex flex-wrap gap-1 mt-1">
{editAppData.inputFields.map((field) => (
<span
key={field}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded"
>
{field}
</span>
))}
</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{editAppData.outputFormat}</div>
</div>
</div>
</Card>
<Card className="p-4 bg-yellow-50 border-yellow-300">
<div className="flex items-center gap-2 text-yellow-800">
<AlertTriangle className="w-4 h-4" />
<span className="text-sm">
</span>
</div>
</Card>
</div>
)}
</div>
{/* 底部按钮 */}
<DialogFooter>
<div className="flex justify-between w-full">
<Button
variant="outline"
onClick={handleClose}
disabled={editStep === 5}
>
</Button>
<div className="flex gap-2">
{editStep > 1 && editStep < 5 && (
<Button variant="outline" onClick={handlePrevStep}>
<ChevronLeft className="w-4 h-4 mr-2" />
</Button>
)}
{editStep < 5 ? (
<Button onClick={handleNextStep}>
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
) : (
<Button onClick={handleSaveEdit} className="bg-green-600 hover:bg-green-700">
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
)}
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,505 @@
/**
* filekorolheader: 应用生成对话框 - 模型应用生成流程对话框
* 功能:多步骤应用生成流程、表单验证、应用发布
* 路径:/ai-crop-model/model-application/generation/components/ApplicationGenerateDialog
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { ApplicationGenerationState, ApplicationGenerationAction } from './ApplicationGenerationReducer';
import { Application, ApplicationType, OutputFormat } from '@/types/ai-model';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import {
Sparkles,
ChevronLeft,
ChevronRight,
FileText,
Server,
BarChart3,
LineChart as LineChartIcon,
PieChart as PieChartIcon,
Table as TableIcon,
Type,
CheckCircle,
} from 'lucide-react';
import { toast } from 'sonner';
interface ApplicationGenerateDialogProps {
state: ApplicationGenerationState;
dispatch: React.Dispatch<ApplicationGenerationAction>;
}
export default function ApplicationGenerateDialog({ state, dispatch }: ApplicationGenerateDialogProps) {
const { showGenerateDialog, generateStep, newAppData, availableModels, inputFieldOptions } = state;
const handleNextStep = () => {
if (generateStep === 1) {
if (!newAppData.name || !newAppData.type || !newAppData.description) {
toast.error('请填写完整的基本信息');
return;
}
} else if (generateStep === 2) {
if (!newAppData.modelName || !newAppData.modelVersion) {
toast.error('请选择模型');
return;
}
} else if (generateStep === 3) {
if (newAppData.inputFields.length === 0) {
toast.error('请选择至少一个输入字段');
return;
}
} else if (generateStep === 4) {
if (!newAppData.outputFormat) {
toast.error('请选择输出格式');
return;
}
}
dispatch({ type: 'SET_GENERATE_STEP', payload: generateStep + 1 });
};
const handlePrevStep = () => {
dispatch({ type: 'SET_GENERATE_STEP', payload: generateStep - 1 });
};
const handlePublishApp = () => {
const newApp: Application = {
id: `app-${Date.now()}`,
name: newAppData.name,
type: newAppData.type as ApplicationType,
description: newAppData.description,
modelName: newAppData.modelName,
modelVersion: newAppData.modelVersion,
inputConfig: {
fields: newAppData.inputFields,
},
outputConfig: {
format: newAppData.outputFormat as OutputFormat,
},
status: '已停止',
createTime: new Date().toISOString().split('T')[0],
runCount: 0,
successRate: 0,
avgExecutionTime: 0,
};
dispatch({ type: 'ADD_APPLICATION', payload: newApp });
dispatch({ type: 'SET_SHOW_GENERATE_DIALOG', payload: false });
dispatch({ type: 'SET_GENERATE_STEP', payload: 1 });
dispatch({ type: 'RESET_NEW_APP_DATA' });
toast.success('应用创建成功!');
};
const handleClose = () => {
dispatch({ type: 'SET_SHOW_GENERATE_DIALOG', payload: false });
dispatch({ type: 'SET_GENERATE_STEP', payload: 1 });
dispatch({ type: 'RESET_NEW_APP_DATA' });
};
const getStepIcon = (step: number) => {
switch (step) {
case 1:
return <FileText className="w-4 h-4" />;
case 2:
return <Server className="w-4 h-4" />;
case 3:
return <FileText className="w-4 h-4" />;
case 4:
return <BarChart3 className="w-4 h-4" />;
default:
return <CheckCircle className="w-4 h-4" />;
}
};
const getStepTitle = (step: number) => {
switch (step) {
case 1:
return '基本信息';
case 2:
return '选择模型';
case 3:
return '输入配置';
case 4:
return '输出格式';
case 5:
return '确认发布';
default:
return '';
}
};
return (
<Dialog open={showGenerateDialog} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-blue-600" />
- {['填写基本信息', '选择模型', '配置输入', '配置输出', '预览发布'][generateStep - 1]}
</DialogTitle>
<DialogDescription>
{generateStep} / 5
</DialogDescription>
</DialogHeader>
{/* 步骤指示器 */}
<div className="flex items-center justify-between mb-6">
{[1, 2, 3, 4, 5].map((step) => (
<div key={step} className="flex items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
step <= generateStep
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{step < generateStep ? <CheckCircle className="w-4 h-4" /> : getStepIcon(step)}
</div>
<span
className={`ml-2 text-sm ${
step <= generateStep ? 'text-blue-600 font-medium' : 'text-gray-500'
}`}
>
{getStepTitle(step)}
</span>
{step < 5 && (
<ChevronRight className="w-4 h-4 mx-4 text-gray-400" />
)}
</div>
))}
</div>
{/* 步骤内容 */}
<div className="min-h-[400px]">
{/* 步骤1: 基本信息 */}
{generateStep === 1 && (
<div className="space-y-4">
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2 text-blue-900 dark:text-blue-100">
<FileText className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<div>
<Label> *</Label>
<Input
placeholder="请输入应用名称,如:智能灌溉策略生成"
value={newAppData.name}
onChange={(e) => dispatch({ type: 'SET_NEW_APP_DATA', payload: { name: e.target.value } })}
/>
</div>
<div>
<Label> *</Label>
<Select
value={newAppData.type}
onValueChange={(value) => dispatch({ type: 'SET_NEW_APP_DATA', payload: { type: value } })}
>
<SelectTrigger>
<SelectValue placeholder="选择应用类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="智能灌溉"></SelectItem>
<SelectItem value="病虫害预警"></SelectItem>
<SelectItem value="施肥推荐"></SelectItem>
<SelectItem value="产量预测"></SelectItem>
<SelectItem value="生长监测"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label> *</Label>
<Textarea
placeholder="请详细描述应用的功能和使用场景"
value={newAppData.description}
onChange={(e) => dispatch({ type: 'SET_NEW_APP_DATA', payload: { description: e.target.value } })}
rows={4}
/>
</div>
</div>
)}
{/* 步骤2: 选择模型 */}
{generateStep === 2 && (
<div className="space-y-4">
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
<div className="flex items-center gap-2 text-purple-900 dark:text-purple-100">
<Server className="w-4 h-4" />
<span className="text-sm">AI模型</span>
</div>
</Card>
<div>
<Label> *</Label>
<div className="grid grid-cols-1 gap-3 mt-2">
{availableModels.map((model) => (
<div
key={model.id}
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
newAppData.modelName === model.name
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600'
}`}
onClick={() =>
dispatch({
type: 'SET_NEW_APP_DATA',
payload: { modelName: model.name, modelVersion: model.version }
})
}
>
<div className="flex items-center justify-between">
<div>
<div className="font-medium">{model.name}</div>
<div className="text-sm text-muted-foreground">
: {model.version} | : {model.type}
</div>
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
model.status === '运行中' ? 'bg-green-500' : 'bg-red-500'
}`} />
<span className="text-sm">{model.status}</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* 步骤3: 输入配置 */}
{generateStep === 3 && (
<div className="space-y-4">
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<div className="flex items-center gap-2 text-green-900 dark:text-green-100">
<FileText className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<div>
<Label> * ()</Label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mt-2 max-h-64 overflow-y-auto p-2 border rounded-lg">
{inputFieldOptions.map((field) => (
<div key={field} className="flex items-center space-x-2">
<Checkbox
id={field}
checked={newAppData.inputFields.includes(field)}
onCheckedChange={(checked) => {
if (checked) {
dispatch({
type: 'SET_NEW_APP_DATA',
payload: {
inputFields: [...newAppData.inputFields, field]
}
});
} else {
dispatch({
type: 'SET_NEW_APP_DATA',
payload: {
inputFields: newAppData.inputFields.filter(f => f !== field)
}
});
}
}}
/>
<Label htmlFor={field} className="text-sm font-normal cursor-pointer">
{field}
</Label>
</div>
))}
</div>
<div className="mt-2 text-sm text-muted-foreground">
{newAppData.inputFields.length}
</div>
</div>
</div>
)}
{/* 步骤4: 输出格式 */}
{generateStep === 4 && (
<div className="space-y-4">
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
<div className="flex items-center gap-2 text-orange-900 dark:text-orange-100">
<BarChart3 className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<div>
<Label> *</Label>
<div className="grid grid-cols-2 gap-3 mt-2">
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
newAppData.outputFormat === '折线图'
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600'
}`}
onClick={() => dispatch({ type: 'SET_NEW_APP_DATA', payload: { outputFormat: '折线图' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<LineChartIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<div className="font-medium">线</div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
newAppData.outputFormat === '饼状图'
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600'
}`}
onClick={() => dispatch({ type: 'SET_NEW_APP_DATA', payload: { outputFormat: '饼状图' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<PieChartIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
newAppData.outputFormat === '表格'
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600'
}`}
onClick={() => dispatch({ type: 'SET_NEW_APP_DATA', payload: { outputFormat: '表格' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<TableIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
<div
className={`p-4 border-2 rounded-lg cursor-pointer transition-all ${
newAppData.outputFormat === '文字'
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600'
}`}
onClick={() => dispatch({ type: 'SET_NEW_APP_DATA', payload: { outputFormat: '文字' } })}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center">
<Type className="w-5 h-5 text-orange-600 dark:text-orange-400" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* 步骤5: 确认发布 */}
{generateStep === 5 && (
<div className="space-y-4">
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<div className="flex items-center gap-2 text-green-900 dark:text-green-100">
<CheckCircle className="w-4 h-4" />
<span className="text-sm"></span>
</div>
</Card>
<Card className="p-6">
<h3 className="font-semibold mb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{newAppData.name}</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{newAppData.type}</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{newAppData.modelName}</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{newAppData.modelVersion}</div>
</div>
<div className="col-span-2">
<Label className="text-sm text-muted-foreground"></Label>
<div className="text-sm">{newAppData.description}</div>
</div>
<div className="col-span-2">
<Label className="text-sm text-muted-foreground"> ({newAppData.inputFields.length})</Label>
<div className="flex flex-wrap gap-1 mt-1">
{newAppData.inputFields.map((field) => (
<span
key={field}
className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs rounded"
>
{field}
</span>
))}
</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="font-medium">{newAppData.outputFormat}</div>
</div>
</div>
</Card>
</div>
)}
</div>
{/* 底部按钮 */}
<DialogFooter>
<div className="flex justify-between w-full">
<Button
variant="outline"
onClick={handleClose}
disabled={generateStep === 5}
>
</Button>
<div className="flex gap-2">
{generateStep > 1 && generateStep < 5 && (
<Button variant="outline" onClick={handlePrevStep}>
<ChevronLeft className="w-4 h-4 mr-2" />
</Button>
)}
{generateStep < 5 ? (
<Button onClick={handleNextStep}>
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
) : (
<Button onClick={handlePublishApp} className="bg-green-600 hover:bg-green-700">
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
)}
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,180 @@
/**
* filekorolheader: 应用生成状态管理 - 模型应用生成中心状态管理
* 功能:应用状态管理、生成流程控制、对话框状态管理
* 路径:/ai-crop-model/model-application/generation/components/ApplicationGenerationReducer
* 规范遵循crop-x/docs/开发项目规范.md使用TypeScript类型安全
*/
'use client';
import { Application } from '@/types/ai-model';
export interface ApplicationGenerationState {
applications: Application[];
showGenerateDialog: boolean;
generateStep: number;
newAppData: {
name: string;
type: string;
description: string;
modelName: string;
modelVersion: string;
inputFields: string[];
outputFormat: string;
};
showRunDialog: boolean;
runningApp: Application | null;
inputData: Record<string, string>;
isRunning: boolean;
runResult: any;
availableModels: Array<{
id: string;
name: string;
version: string;
type: string;
status: string;
}>;
inputFieldOptions: string[];
dataSourceOptions: string[];
iotDeviceOptions: string[];
showEditDialog: boolean;
editingApp: Application | null;
editStep: number;
editAppData: {
name: string;
type: string;
description: string;
modelName: string;
modelVersion: string;
inputFields: string[];
outputFormat: string;
};
}
export type ApplicationGenerationAction =
| { type: 'SET_APPLICATIONS'; payload: Application[] }
| { type: 'ADD_APPLICATION'; payload: Application }
| { type: 'UPDATE_APPLICATION'; payload: { id: string; updates: Partial<Application> } }
| { type: 'SET_SHOW_GENERATE_DIALOG'; payload: boolean }
| { type: 'SET_GENERATE_STEP'; payload: number }
| { type: 'SET_NEW_APP_DATA'; payload: Partial<ApplicationGenerationState['newAppData']> }
| { type: 'RESET_NEW_APP_DATA' }
| { type: 'SET_SHOW_RUN_DIALOG'; payload: boolean }
| { type: 'SET_RUNNING_APP'; payload: Application | null }
| { type: 'SET_INPUT_DATA'; payload: Record<string, string> }
| { type: 'SET_IS_RUNNING'; payload: boolean }
| { type: 'SET_RUN_RESULT'; payload: any }
| { type: 'TOGGLE_APPLICATION_STATUS'; payload: string }
| { type: 'SET_SHOW_EDIT_DIALOG'; payload: boolean }
| { type: 'SET_EDITING_APP'; payload: Application | null }
| { type: 'SET_EDIT_STEP'; payload: number }
| { type: 'SET_EDIT_APP_DATA'; payload: Partial<ApplicationGenerationState['editAppData']> }
| { type: 'RESET_EDIT_APP_DATA' };
export function ApplicationGenerationReducer(
state: ApplicationGenerationState,
action: ApplicationGenerationAction
): ApplicationGenerationState {
switch (action.type) {
case 'SET_APPLICATIONS':
return { ...state, applications: action.payload };
case 'ADD_APPLICATION':
return {
...state,
applications: [...state.applications, action.payload]
};
case 'UPDATE_APPLICATION':
return {
...state,
applications: state.applications.map(app =>
app.id === action.payload.id
? { ...app, ...action.payload.updates }
: app
)
};
case 'TOGGLE_APPLICATION_STATUS':
return {
...state,
applications: state.applications.map(app =>
app.id === action.payload
? { ...app, status: app.status === '运行中' ? '已停止' : '运行中' }
: app
)
};
case 'SET_SHOW_GENERATE_DIALOG':
return { ...state, showGenerateDialog: action.payload };
case 'SET_GENERATE_STEP':
return { ...state, generateStep: action.payload };
case 'SET_NEW_APP_DATA':
return {
...state,
newAppData: { ...state.newAppData, ...action.payload }
};
case 'RESET_NEW_APP_DATA':
return {
...state,
newAppData: {
name: '',
type: '',
description: '',
modelName: '',
modelVersion: '',
inputFields: [],
outputFormat: '',
}
};
case 'SET_SHOW_RUN_DIALOG':
return { ...state, showRunDialog: action.payload };
case 'SET_RUNNING_APP':
return { ...state, runningApp: action.payload };
case 'SET_INPUT_DATA':
return { ...state, inputData: action.payload };
case 'SET_IS_RUNNING':
return { ...state, isRunning: action.payload };
case 'SET_RUN_RESULT':
return { ...state, runResult: action.payload };
case 'SET_SHOW_EDIT_DIALOG':
return { ...state, showEditDialog: action.payload };
case 'SET_EDITING_APP':
return { ...state, editingApp: action.payload };
case 'SET_EDIT_STEP':
return { ...state, editStep: action.payload };
case 'SET_EDIT_APP_DATA':
return {
...state,
editAppData: { ...state.editAppData, ...action.payload }
};
case 'RESET_EDIT_APP_DATA':
return {
...state,
editAppData: {
name: '',
type: '',
description: '',
modelName: '',
modelVersion: '',
inputFields: [],
outputFormat: '',
}
};
default:
return state;
}
}

View File

@@ -0,0 +1,303 @@
/**
* filekorolheader: 应用列表组件 - 模型应用列表展示
* 功能:应用列表展示、状态管理、操作按钮
* 路径:/ai-crop-model/model-application/generation/components/ApplicationList
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { ApplicationGenerationState, ApplicationGenerationAction } from './ApplicationGenerationReducer';
import { Application } from '@/types/ai-model';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
PlayCircle,
PauseCircle,
StopCircle,
Eye,
Edit,
Clock,
CheckCircle,
XCircle,
AlertTriangle,
Droplets,
Bug,
Sprout,
TrendingUp,
Activity,
Server,
Terminal,
LineChart as LineChartIcon,
PieChart as PieChartIcon,
Table as TableIcon,
Type,
Rocket,
} from 'lucide-react';
import { toast } from 'sonner';
interface ApplicationListProps {
state: ApplicationGenerationState;
dispatch: React.Dispatch<ApplicationGenerationAction>;
}
export default function ApplicationList({ state, dispatch }: ApplicationListProps) {
// 辅助函数
const getAppTypeIcon = (type: string) => {
switch (type) {
case '智能灌溉': return <Droplets className="w-4 h-4 text-green-600" />;
case '病虫害预警': return <Bug className="w-4 h-4 text-orange-600" />;
case '施肥推荐': return <Sprout className="w-4 h-4 text-purple-600" />;
case '产量预测': return <TrendingUp className="w-4 h-4 text-blue-600" />;
case '生长监测': return <Activity className="w-4 h-4 text-cyan-600" />;
default: return <Server className="w-4 h-4 text-gray-600" />;
}
};
const getOutputFormatIcon = (format: string) => {
switch (format) {
case '折线图': return <LineChartIcon className="w-4 h-4 text-blue-600" />;
case '饼状图': return <PieChartIcon className="w-4 h-4 text-green-600" />;
case '表格': return <TableIcon className="w-4 h-4 text-purple-600" />;
case '文字': return <Type className="w-4 h-4 text-gray-600" />;
default: return <Server className="w-4 h-4 text-gray-600" />;
}
};
const handleRunApplication = (app: Application) => {
dispatch({ type: 'SET_RUNNING_APP', payload: app });
dispatch({ type: 'SET_SHOW_RUN_DIALOG', payload: true });
dispatch({ type: 'SET_INPUT_DATA', payload: {} });
dispatch({ type: 'SET_RUN_RESULT', payload: null });
};
const handleToggleStatus = (appId: string) => {
dispatch({ type: 'TOGGLE_APPLICATION_STATUS', payload: appId });
const app = state.applications.find(a => a.id === appId);
toast.success(`应用已${app?.status === '运行中' ? '停止' : '启动'}`);
};
const handleEditApp = (app: Application) => {
dispatch({ type: 'SET_EDITING_APP', payload: app });
dispatch({
type: 'SET_EDIT_APP_DATA',
payload: {
name: app.name,
type: app.type,
description: app.description,
modelName: app.modelName,
modelVersion: app.modelVersion,
inputFields: app.inputConfig.fields,
outputFormat: app.outputConfig.format,
}
});
dispatch({ type: 'SET_EDIT_STEP', payload: 1 });
dispatch({ type: 'SET_SHOW_EDIT_DIALOG', payload: true });
};
const getStatusIcon = (status: string) => {
switch (status) {
case '运行中':
return <PlayCircle className="w-4 h-4 text-green-600" />;
case '已停止':
return <StopCircle className="w-4 h-4 text-gray-600" />;
case '故障':
return <XCircle className="w-4 h-4 text-red-600" />;
default:
return <AlertCircle className="w-4 h-4 text-gray-600" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case '运行中':
return 'bg-green-100 text-green-700 border-green-300';
case '已停止':
return 'bg-gray-100 text-gray-700 border-gray-300';
case '故障':
return 'bg-red-100 text-red-700 border-red-300';
default:
return 'bg-gray-100 text-gray-700 border-gray-300';
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case '运行中':
return (
<Badge className="bg-green-100 text-green-800 border-green-200 font-light">
<CheckCircle className="w-3 h-3 mr-1" />
</Badge>
);
case '已停止':
return (
<Badge className="bg-red-100 text-red-800 border-red-200 font-light">
<XCircle className="w-3 h-3 mr-1" />
</Badge>
);
case '故障':
return (
<Badge className="bg-orange-100 text-orange-800 border-orange-200 font-light">
<AlertTriangle className="w-3 h-3 mr-1" />
</Badge>
);
default:
return (
<Badge variant="outline" className="font-light">
<Clock className="w-3 h-3 mr-1" />
</Badge>
);
}
};
const getTypeColor = (type: string) => {
switch (type) {
case '智能灌溉':
return 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800';
case '病虫害预警':
return 'bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800';
case '施肥推荐':
return 'bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800';
case '产量预测':
return 'bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800';
case '生长监测':
return 'bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800';
default:
return 'bg-gray-50 dark:bg-gray-950 border-gray-200 dark:border-gray-800';
}
};
return (
<div>
<h3 className="mb-4 flex items-center gap-2">
<Rocket className="w-5 h-5 text-blue-600" />
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{state.applications.map((app) => (
<Card key={app.id} className="p-6 hover:shadow-lg transition-shadow">
<div className="space-y-4">
{/* 头部 */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-50 rounded-lg flex items-center justify-center">
{getAppTypeIcon(app.type)}
</div>
<div>
<h4 className="text-base">{app.name}</h4>
<Badge variant="outline" className="mt-1">{app.type}</Badge>
</div>
</div>
</div>
{/* 描述 */}
<p className="text-sm text-muted-foreground line-clamp-2">
{app.description}
</p>
{/* 模型信息 */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Server className="w-4 h-4 text-gray-500" />
<span className="text-muted-foreground">:</span>
<span>{app.modelName}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Terminal className="w-4 h-4 text-gray-500" />
<span className="text-muted-foreground">:</span>
<span>{app.modelVersion}</span>
</div>
</div>
{/* 输入字段 */}
<div>
<p className="text-xs text-muted-foreground mb-2">:</p>
<div className="flex flex-wrap gap-1">
{app.inputConfig.fields.slice(0, 3).map((field, idx) => (
<Badge key={idx} variant="outline" className="text-xs">
{field}
</Badge>
))}
{app.inputConfig.fields.length > 3 && (
<Badge variant="outline" className="text-xs">
+{app.inputConfig.fields.length - 3}
</Badge>
)}
</div>
</div>
{/* 输出格式 */}
<div className="flex items-center gap-2">
{getOutputFormatIcon(app.outputConfig.format)}
<span className="text-sm text-muted-foreground">:</span>
<Badge className="bg-purple-100 text-purple-700 border-purple-300">
{app.outputConfig.format}
</Badge>
</div>
{/* 统计信息 */}
<div className="pt-4 border-t">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-lg">{app.runCount}</p>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-lg text-green-600">{app.successRate}%</p>
</div>
</div>
</div>
{/* 状态和操作 */}
<div className="space-y-3 pt-4 border-t">
<div className="flex items-center gap-2">
{getStatusIcon(app.status)}
<Badge className={getStatusColor(app.status)}>{app.status}</Badge>
</div>
<div className="flex gap-2">
{/* 运行按钮 - 仅在非停止状态显示 */}
{app.status !== '已停止' && (
<Button
size="sm"
className="flex-1 bg-blue-600 hover:bg-blue-700"
onClick={() => handleRunApplication(app)}
>
<PlayCircle className="w-3 h-3 mr-1" />
</Button>
)}
{/* 停止/启动按钮 */}
{app.status === '运行中' ? (
<Button
size="sm"
variant="outline"
className={app.status === '已停止' ? 'flex-1' : ''}
onClick={() => handleToggleStatus(app.id)}
>
<PauseCircle className="w-3 h-3" />
</Button>
) : (
<Button
size="sm"
variant="outline"
className={app.status === '已停止' ? 'flex-1' : ''}
onClick={() => handleToggleStatus(app.id)}
>
<PlayCircle className="w-3 h-3" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => {/* 查看详情 */}}>
<Eye className="w-3 h-3" />
</Button>
<Button size="sm" variant="outline" onClick={() => handleEditApp(app)}>
<Edit className="w-3 h-3" />
</Button>
</div>
</div>
</div>
</Card>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,455 @@
/**
* filekorolheader: 应用运行对话框 - 模型应用运行和结果展示
* 功能:应用参数输入、运行执行、结果可视化展示
* 路径:/ai-crop-model/model-application/generation/components/ApplicationRunDialog
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { useState } from 'react';
import { ApplicationGenerationState, ApplicationGenerationAction } from './ApplicationGenerationReducer';
import { Application } from '@/types/ai-model';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import {
PlayCircle,
StopCircle,
Loader2,
CheckCircle,
XCircle,
AlertTriangle,
LineChart as LineChartIcon,
PieChart as PieChartIcon,
Table as TableIcon,
FileText,
} from 'lucide-react';
import { toast } from 'sonner';
import {
LineChart,
Line,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
interface ApplicationRunDialogProps {
state: ApplicationGenerationState;
dispatch: React.Dispatch<ApplicationGenerationAction>;
}
export default function ApplicationRunDialog({ state, dispatch }: ApplicationRunDialogProps) {
const { showRunDialog, runningApp, inputData, isRunning, runResult, dataSourceOptions, iotDeviceOptions } = state;
const [selectedDataSources, setSelectedDataSources] = useState<Record<string, string>>({});
if (!runningApp) return null;
const handleInputChange = (field: string, value: string) => {
dispatch({
type: 'SET_INPUT_DATA',
payload: { ...inputData, [field]: value }
});
};
const handleDataSourceChange = (field: string, dataSource: string) => {
setSelectedDataSources(prev => ({ ...prev, [field]: dataSource }));
};
const handleRunApplication = async () => {
// 验证必填字段
const missingFields = runningApp.inputConfig.fields.filter(field => !inputData[field]);
if (missingFields.length > 0) {
toast.error(`请填写以下字段: ${missingFields.join(', ')}`);
return;
}
dispatch({ type: 'SET_IS_RUNNING', payload: true });
dispatch({ type: 'SET_RUN_RESULT', payload: null });
try {
// 模拟应用运行过程
await new Promise(resolve => setTimeout(resolve, 2000));
// 生成模拟结果数据
const mockResult = generateMockResult(runningApp);
dispatch({ type: 'SET_RUN_RESULT', payload: mockResult });
toast.success('应用运行成功!');
} catch (error) {
toast.error('应用运行失败');
} finally {
dispatch({ type: 'SET_IS_RUNNING', payload: false });
}
};
const generateMockResult = (app: Application) => {
switch (app.outputConfig.format) {
case '折线图':
return {
type: 'line',
data: [
{ name: '1月', value: Math.random() * 100 },
{ name: '2月', value: Math.random() * 100 },
{ name: '3月', value: Math.random() * 100 },
{ name: '4月', value: Math.random() * 100 },
{ name: '5月', value: Math.random() * 100 },
{ name: '6月', value: Math.random() * 100 },
],
analysis: `${app.name}分析完成,建议继续当前策略。`
};
case '饼状图':
return {
type: 'pie',
data: [
{ name: '正常', value: 65, color: '#10b981' },
{ name: '注意', value: 25, color: '#f59e0b' },
{ name: '警告', value: 10, color: '#ef4444' },
],
analysis: `${app.name}分析完成65%区域状态正常需要关注25%区域。`
};
case '表格':
return {
type: 'table',
data: [
{ : '土壤湿度', : '65%', : '正常', : '保持当前灌溉策略' },
{ : '氮含量', : '120mg/kg', : '偏低', : '建议补充氮肥' },
{ : 'pH值', : '6.8', : '正常', : '无需调节' },
{ : '温度', : '25°C', : '适宜', : '适合作物生长' },
],
analysis: `${app.name}分析完成,各项指标基本正常,建议适当补充氮肥。`
};
case '文字':
return {
type: 'text',
content: `
## ${app.name}分析报告
### 总体评估
根据当前输入数据分析,${app.name}系统运行状态良好。
### 详细分析
1. **环境条件**: 当前温度25°C湿度65%pH值6.8,均处于适宜范围。
2. **营养状况**: 氮含量120mg/kg略低于推荐值建议适当补充。
3. **水分管理**: 土壤湿度65%,处于理想状态,可维持当前灌溉策略。
### 建议措施
- 建议在未来一周内补充氮肥用量为15kg/亩
- 继续保持当前灌溉策略,每周监测土壤湿度变化
- 密切关注天气变化,如遇降雨需调整灌溉计划
### 预期效果
按照建议措施实施预计作物产量可提升5-8%,品质得到改善。
`,
analysis: `${app.name}综合分析报告生成完成。`
};
default:
return null;
}
};
const handleClose = () => {
dispatch({ type: 'SET_SHOW_RUN_DIALOG', payload: false });
dispatch({ type: 'SET_RUNNING_APP', payload: null });
dispatch({ type: 'SET_INPUT_DATA', payload: {} });
dispatch({ type: 'SET_RUN_RESULT', payload: null });
setSelectedDataSources({});
};
const renderOutputResult = () => {
if (!runResult) return null;
switch (runResult.type) {
case 'line':
return (
<div className="space-y-4">
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={runResult.data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<RechartsTooltip />
<Legend />
<Line
type="monotone"
dataKey="value"
stroke="#3b82f6"
strokeWidth={2}
dot={{ fill: '#3b82f6' }}
/>
</LineChart>
</ResponsiveContainer>
</div>
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-900 dark:text-blue-100">{runResult.analysis}</p>
</Card>
</div>
);
case 'pie':
return (
<div className="space-y-4">
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={runResult.data}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{runResult.data.map((entry: any, index: number) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<RechartsTooltip />
</PieChart>
</ResponsiveContainer>
</div>
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<p className="text-sm text-green-900 dark:text-green-100">{runResult.analysis}</p>
</Card>
</div>
);
case 'table':
return (
<div className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{runResult.data.map((row: any, index: number) => (
<TableRow key={index}>
<TableCell className="font-medium">{row.}</TableCell>
<TableCell>{row.}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs ${
row. === '正常'
? 'bg-green-100 text-green-800'
: row. === '偏低'
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}>
{row.}
</span>
</TableCell>
<TableCell className="text-sm">{row.}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
<p className="text-sm text-purple-900 dark:text-purple-100">{runResult.analysis}</p>
</Card>
</div>
);
case 'text':
return (
<div className="space-y-4">
<Card className="p-6">
<div className="prose prose-sm max-w-none dark:prose-invert">
<pre className="whitespace-pre-wrap text-sm font-sans">
{runResult.content}
</pre>
</div>
</Card>
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
<p className="text-sm text-orange-900 dark:text-orange-100">{runResult.analysis}</p>
</Card>
</div>
);
default:
return null;
}
};
return (
<Dialog open={showRunDialog} onOpenChange={handleClose}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<PlayCircle className="w-5 h-5 text-blue-600" />
{runningApp.name}
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 应用信息 */}
<Card className="p-4 bg-gradient-to-r from-blue-50 to-cyan-50 border-blue-200">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<Label className="text-xs text-muted-foreground"></Label>
<div className="font-medium">{runningApp.type}</div>
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<div className="font-medium">{runningApp.modelName}</div>
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<div className="font-mono text-xs">{runningApp.modelVersion}</div>
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<div className="font-medium">{runningApp.outputConfig.format}</div>
</div>
</div>
</Card>
{/* 输入参数 */}
{!runResult && (
<Card className="p-4">
<h3 className="font-semibold mb-4"></h3>
<div className="space-y-4">
{runningApp.inputConfig.fields.map((field) => (
<div key={field} className="space-y-2">
<Label>{field} *</Label>
<div className="flex gap-2">
<div className="flex-1">
{field.includes('图像') || field.includes('图片') ? (
<Textarea
placeholder={`请输入${field}的描述或上传图片链接`}
value={inputData[field] || ''}
onChange={(e) => handleInputChange(field, e.target.value)}
rows={3}
/>
) : field.includes('选择') || field.includes('类型') ? (
<Select
value={inputData[field] || ''}
onValueChange={(value) => handleInputChange(field, value)}
>
<SelectTrigger>
<SelectValue placeholder={`请选择${field}`} />
</SelectTrigger>
<SelectContent>
<SelectItem value="选项1">1</SelectItem>
<SelectItem value="选项2">2</SelectItem>
<SelectItem value="选项3">3</SelectItem>
</SelectContent>
</Select>
) : field.includes('时间') || field.includes('日期') ? (
<Input
type="datetime-local"
value={inputData[field] || ''}
onChange={(e) => handleInputChange(field, e.target.value)}
/>
) : (
<Input
placeholder={`请输入${field}`}
value={inputData[field] || ''}
onChange={(e) => handleInputChange(field, e.target.value)}
/>
)}
</div>
<div className="w-48">
<Select
value={selectedDataSources[field] || ''}
onValueChange={(value) => handleDataSourceChange(field, value)}
>
<SelectTrigger>
<SelectValue placeholder="数据来源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="手动输入"></SelectItem>
{dataSourceOptions.map((source) => (
<SelectItem key={source} value={source}>
{source}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{selectedDataSources[field] && selectedDataSources[field] !== '手动输入' && (
<div className="text-xs text-muted-foreground">
: {selectedDataSources[field]}
{selectedDataSources[field].includes('传感器') && (
<Select defaultValue="">
<SelectTrigger className="w-full mt-1">
<SelectValue placeholder="选择设备" />
</SelectTrigger>
<SelectContent>
{iotDeviceOptions.map((device) => (
<SelectItem key={device} value={device}>
{device}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
)}
</div>
))}
</div>
</Card>
)}
{/* 运行结果 */}
{runResult && (
<Card className="p-4">
<div className="flex items-center gap-2 mb-4">
<CheckCircle className="w-5 h-5 text-green-600" />
<h3 className="font-semibold"></h3>
</div>
{renderOutputResult()}
</Card>
)}
</div>
<DialogFooter>
<div className="flex justify-between w-full">
<Button variant="outline" onClick={handleClose}>
{runResult ? '关闭' : '取消'}
</Button>
{!runResult && (
<Button
onClick={handleRunApplication}
disabled={isRunning}
className="bg-blue-600 hover:bg-blue-700"
>
{isRunning ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<PlayCircle className="w-4 h-4 mr-2" />
</>
)}
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,303 @@
/**
* filekorolheader: 模型应用中心生成页面 - AI模型应用生成管理
* 功能:应用生成、应用管理、应用运行、结果展示
* 路径:/ai-crop-model/model-application/generation
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式
*/
'use client';
import { useReducer } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import {
Rocket,
Plus,
CheckCircle,
XCircle,
AlertCircle,
PlayCircle,
PauseCircle,
StopCircle,
Eye,
Edit,
Clock,
Zap,
BarChart3,
Activity,
Droplets,
Bug,
Sprout,
Terminal,
FileText,
Timer,
TrendingUp,
Server,
Cpu,
AlertTriangle,
CheckCircle2,
ChevronRight,
ChevronLeft,
Sparkles,
Table as TableIcon,
Type,
PieChart as PieChartIcon,
LineChart as LineChartIcon,
} from 'lucide-react';
import { toast } from 'sonner';
import {
LineChart,
Line,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { ApplicationGenerationReducer, ApplicationGenerationState, ApplicationGenerationAction } from './components/ApplicationGenerationReducer';
import ApplicationGenerateDialog from './components/ApplicationGenerateDialog';
import ApplicationRunDialog from './components/ApplicationRunDialog';
import ApplicationList from './components/ApplicationList';
import ApplicationEditDialog from './components/ApplicationEditDialog';
export default function GenerationPage() {
const [state, dispatch] = useReducer(ApplicationGenerationReducer, {
applications: [
{
id: 'app-1',
name: '智能灌溉策略生成',
type: '智能灌溉',
description: '基于土壤湿度、天气预报和作物需水模型,自动生成最优灌溉方案',
modelName: '灌溉优化模型',
modelVersion: 'v2.1.0',
inputConfig: {
fields: ['土壤湿度', '温度', '降水预报', '作物类型', '生长阶段'],
},
outputConfig: {
format: '折线图',
},
status: '运行中',
createTime: '2024-01-15',
lastRunTime: '2024-03-20 14:30',
runCount: 156,
successRate: 98.5,
avgExecutionTime: 2.3,
},
{
id: 'app-2',
name: '病虫害智能预警',
type: '病虫害预警',
description: '通过图像识别和环境数据分析,提前预警病虫害风险',
modelName: '病虫害识别模型',
modelVersion: 'v1.8.0',
inputConfig: {
fields: ['作物图像', '温度', '湿度', '叶片状况', '历史发病记录'],
},
outputConfig: {
format: '饼状图',
},
status: '运行中',
createTime: '2024-01-20',
lastRunTime: '2024-03-20 16:45',
runCount: 89,
successRate: 96.2,
avgExecutionTime: 1.8,
},
{
id: 'app-3',
name: '精准施肥推荐',
type: '施肥推荐',
description: '根据土壤养分和作物需求,生成精准施肥方案',
modelName: '营养需求模型',
modelVersion: 'v1.5.0',
inputConfig: {
fields: ['土壤养分', '作物类型', '生长阶段', '目标产量', '历史施肥记录'],
},
outputConfig: {
format: '表格',
},
status: '已停止',
createTime: '2024-02-01',
lastRunTime: '2024-03-18 09:15',
runCount: 45,
successRate: 94.7,
avgExecutionTime: 3.1,
},
],
showGenerateDialog: false,
generateStep: 1,
newAppData: {
name: '',
type: '',
description: '',
modelName: '',
modelVersion: '',
inputFields: [],
outputFormat: '',
},
showRunDialog: false,
runningApp: null,
inputData: {},
isRunning: false,
runResult: null,
showEditDialog: false,
editingApp: null,
editStep: 1,
editAppData: {
name: '',
type: '',
description: '',
modelName: '',
modelVersion: '',
inputFields: [],
outputFormat: '',
},
availableModels: [
{ id: 'm1', name: '灌溉优化模型', version: 'v2.1.0', type: 'TensorFlow', status: '运行中' },
{ id: 'm2', name: '病虫害识别模型', version: 'v1.8.0', type: 'PyTorch', status: '运行中' },
{ id: 'm3', name: '营养需求模型', version: 'v1.5.0', type: 'TensorFlow', status: '运行中' },
{ id: 'm4', name: '产量预测模型', version: 'v2.0.0', type: 'XGBoost', status: '运行中' },
{ id: 'm5', name: '生长监测模型', version: 'v1.3.0', type: 'PyTorch', status: '运行中' },
{ id: 'm6', name: '天气预测模型', version: 'v1.6.0', type: 'LSTM', status: '运行中' },
],
inputFieldOptions: [
'土壤湿度', '温度', '湿度', '光照强度', 'pH值', '电导率', '氮含量', '磷含量', '钾含量',
'降水预报', '风速', '风向', '大气压力', '作物图像', '叶片状况', '株高', '叶面积指数',
'作物类型', '生长阶段', '种植密度', '目标产量', '历史发病记录', '历史施肥记录', '土壤类型',
'地理位置', '海拔高度', '坡度', '坡向', '土壤容重', '有机质含量', '微量元素含量',
'病虫害图像', '虫害密度', '病情指数', '天敌数量', '农药使用记录', '施肥记录', '灌溉记录',
'农事操作记录', '气象数据', '卫星遥感数据', '无人机影像数据', '传感器实时数据',
],
dataSourceOptions: [
'传感器实时数据',
'数据库查询',
'手动输入',
'第三方API',
'历史数据',
'配置文件',
'IoT设备',
'气象站',
],
iotDeviceOptions: [
'土壤传感器01 (SN:SS-001)',
'温湿度传感器02 (SN:TH-002)',
'光照传感器03 (SN:LS-003)',
'pH传感器04 (SN:PH-004)',
'水肥一体机05 (SN:WF-005)',
'气象站06 (SN:WS-006)',
'作物图像采集器07 (SN:IC-007)',
'水泵控制器08 (SN:PC-008)',
],
});
return (
<div className="space-y-6">
{/* 页面标题和描述 */}
<Card className="p-6 bg-gradient-to-r from-blue-50 to-cyan-50 border-blue-200">
<div className="flex items-center gap-3 mb-4">
<Rocket className="w-8 h-8 text-blue-600" />
<div>
<h1 className="text-2xl font-bold text-blue-900"></h1>
<p className="text-blue-700">AI模型应用生成与管理平台</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-blue-600">{state.applications.length}</p>
<p className="text-xs text-blue-600 mt-1"></p>
</div>
<Rocket className="w-12 h-12 text-blue-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-green-600">
{state.applications.filter(app => app.status === '运行中').length}
</p>
<p className="text-xs text-green-600 mt-1"></p>
</div>
<PlayCircle className="w-12 h-12 text-green-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-purple-600">
{state.applications.reduce((sum, app) => sum + app.runCount, 0)}
</p>
<p className="text-xs text-purple-600 mt-1"></p>
</div>
<Zap className="w-12 h-12 text-purple-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-orange-600">
{(state.applications.reduce((sum, app) => sum + app.successRate, 0) / state.applications.length).toFixed(1)}%
</p>
<p className="text-xs text-orange-600 mt-1"></p>
</div>
<CheckCircle2 className="w-12 h-12 text-orange-600 opacity-50" />
</div>
</Card>
</div>
</Card>
{/* 应用生成功能说明 */}
<Card className="p-4 bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950 dark:to-cyan-950 border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-2">
<Rocket className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-900">
<p className="mb-2"></p>
<ul className="space-y-1 text-xs">
<li> <strong></strong>: </li>
<li> <strong></strong>: </li>
<li> <strong></strong>: 线</li>
<li> <strong></strong>: AI模型与农业生产场景深度融合</li>
</ul>
</div>
</div>
<div className="mt-4">
<Button
onClick={() => dispatch({ type: 'SET_SHOW_GENERATE_DIALOG', payload: true })}
className="bg-blue-600 hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
{/* 应用列表 */}
<ApplicationList state={state} dispatch={dispatch} />
{/* 应用生成对话框 */}
<ApplicationGenerateDialog state={state} dispatch={dispatch} />
{/* 应用运行对话框 */}
<ApplicationRunDialog state={state} dispatch={dispatch} />
{/* 应用编辑对话框 */}
<ApplicationEditDialog state={state} dispatch={dispatch} />
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ModelApplicationPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/model-application
</p>
</div>
</Card>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,217 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import {
Package,
Plus,
CheckCircle,
AlertCircle,
RefreshCw,
Trash2,
Clock,
Settings,
Download,
} from 'lucide-react';
interface ModelService {
id: string;
name: string;
dependencies: string[];
}
interface DependencyManageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
}
export function DependencyManageDialog({ open, onOpenChange, model }: DependencyManageDialogProps) {
const handleUpdateDependency = () => {
toast.success('依赖已更新');
};
const handleRemoveDependency = (dep: string) => {
toast.success(`依赖 ${dep} 已移除`);
};
const handleAddDependency = () => {
toast.success('新依赖已添加');
};
if (!model) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> - {model.name}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 当前依赖 */}
<Card className="p-4">
<div className="flex items-center justify-between mb-4">
<h4 className="flex items-center gap-2">
<Package className="w-4 h-4 text-blue-600 dark:text-blue-400" />
({model.dependencies.length})
</h4>
<Button size="sm" variant="outline" onClick={handleAddDependency}>
<Plus className="w-3 h-3 mr-1" />
</Button>
</div>
<div className="space-y-2">
{model.dependencies.map((dep, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900 rounded-lg group">
<div className="flex items-center gap-3 flex-1">
<Package className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" />
<code className="font-mono text-sm flex-1">{dep}</code>
<Badge variant="outline" className="text-xs font-light"></Badge>
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button size="sm" variant="ghost" onClick={handleUpdateDependency} title="更新版本">
<RefreshCw className="w-3 h-3" />
</Button>
<Button size="sm" variant="ghost" onClick={() => handleRemoveDependency(dep)} title="移除">
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</Card>
{/* 依赖检查 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
</h4>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950 rounded-lg">
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"></Badge>
</div>
<Button variant="outline" className="w-full">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
{/* 依赖冲突检测 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
</h4>
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<div className="font-medium text-yellow-900 dark:text-yellow-100 mb-2"></div>
<div className="text-sm text-yellow-800 dark:text-yellow-200 space-y-1">
<div> numpy==1.24.0 tensorflow==2.13.0 </div>
<div> numpy 1.24.3 </div>
</div>
<Button size="sm" className="mt-3" variant="outline">
</Button>
</div>
</div>
</div>
</Card>
{/* 环境配置 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Settings className="w-4 h-4 text-purple-600 dark:text-purple-400" />
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Python版本</Label>
<Select defaultValue="3.9">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3.8">Python 3.8</SelectItem>
<SelectItem value="3.9">Python 3.9</SelectItem>
<SelectItem value="3.10">Python 3.10</SelectItem>
<SelectItem value="3.11">Python 3.11</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>CUDA版本</Label>
<Select defaultValue="11.8">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">使CUDA</SelectItem>
<SelectItem value="11.7">CUDA 11.7</SelectItem>
<SelectItem value="11.8">CUDA 11.8</SelectItem>
<SelectItem value="12.0">CUDA 12.0</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Card>
{/* 依赖更新日志 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</h4>
<div className="space-y-2 max-h-[150px] overflow-y-auto">
{[
{ date: '2024-10-20', action: '更新 tensorflow 2.12.0 → 2.13.0', user: '张三' },
{ date: '2024-10-15', action: '添加 opencv-python==4.8.0', user: '李四' },
{ date: '2024-10-10', action: '更新 numpy 1.23.0 → 1.24.0', user: '王五' },
].map((log, idx) => (
<div key={idx} className="flex items-start gap-3 p-2 text-sm">
<Clock className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<div className="text-xs text-muted-foreground">{log.date}</div>
<div>{log.action}</div>
<div className="text-xs text-muted-foreground">by {log.user}</div>
</div>
</div>
))}
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600">
<Download className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,226 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Card } from '@/components/ui/card';
import { toast } from 'sonner';
import {
Server,
CheckCircle,
Eye,
} from 'lucide-react';
interface DeployConfigDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function DeployConfigDialog({ open, onOpenChange }: DeployConfigDialogProps) {
const handleDeploy = () => {
toast.success('模型部署已启动预计3-5分钟完成');
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 部署环境 */}
<Card className="p-4">
<h4 className="mb-4"></h4>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Select defaultValue="production">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="development"></SelectItem>
<SelectItem value="staging"></SelectItem>
<SelectItem value="production"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select defaultValue="cn-east">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="cn-east"></SelectItem>
<SelectItem value="cn-north"></SelectItem>
<SelectItem value="cn-south"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Card>
{/* 资源配置 */}
<Card className="p-4">
<h4 className="mb-4"></h4>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>CPU配置</Label>
<Select defaultValue="2">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="8">8</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select defaultValue="4">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2GB</SelectItem>
<SelectItem value="4">4GB</SelectItem>
<SelectItem value="8">8GB</SelectItem>
<SelectItem value="16">16GB</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>GPU配置</Label>
<Select defaultValue="none">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">使GPU</SelectItem>
<SelectItem value="t4">NVIDIA T4</SelectItem>
<SelectItem value="v100">NVIDIA V100</SelectItem>
<SelectItem value="a100">NVIDIA A100</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="3" min="1" max="10" />
</div>
</div>
</Card>
{/* 自动伸缩 */}
<Card className="p-4">
<h4 className="mb-4"></h4>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
<Switch defaultChecked />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input type="number" defaultValue="2" min="1" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="10" max="50" />
</div>
<div>
<Label>CPU</Label>
<Input type="number" defaultValue="70" min="0" max="100" />
</div>
<div>
<Label>CPU</Label>
<Input type="number" defaultValue="30" min="0" max="100" />
</div>
</div>
</div>
</Card>
{/* 健康检查 */}
<Card className="p-4">
<h4 className="mb-4"></h4>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input type="number" defaultValue="30" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="10" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="3" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="1" />
</div>
</div>
</Card>
{/* 部署进度预估 */}
<Card className="p-4 bg-gradient-to-r from-green-50 to-teal-50 dark:from-green-950 dark:to-teal-950">
<div className="flex items-start gap-3">
<Server className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="text-green-900 dark:text-green-100 mb-2"></h4>
<div className="space-y-2 text-xs text-green-800 dark:text-green-200">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
<span>1. (~1)</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
<span>2. (~2)</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
<span>3. (~1)</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
<span>4. (~1)</span>
</div>
<p className="mt-2 text-green-600 dark:text-green-400">预计总时间: 3-5</p>
</div>
</div>
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="outline">
<Eye className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600" onClick={handleDeploy}>
<Server className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,257 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { toast } from 'sonner';
import {
Brain,
BarChart3,
Link,
Package,
Terminal,
CheckCircle,
GitBranch,
Copy,
Eye,
} from 'lucide-react';
interface ModelService {
id: string;
name: string;
version: string;
type: string;
format: string;
description: string;
author: string;
createTime: string;
lastUpdateTime: string;
status: string;
endpoint: string;
accessLevel: string;
tags: string[];
accuracy?: number;
inferenceTime?: number;
requestCount: number;
successRate: number;
dependencies: string[];
}
interface ModelDetailDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
}
export function ModelDetailDialog({ open, onOpenChange, model }: ModelDetailDialogProps) {
const handleCopyEndpoint = async (endpoint: string) => {
try {
await navigator.clipboard.writeText(endpoint);
toast.success('端点已复制到剪贴板');
} catch (error) {
toast.error('复制失败,请重试');
}
};
const handleTestModel = () => {
toast.success('模型测试成功,推理正常');
};
const getAccessLevelIcon = (level: string) => {
switch (level) {
case '公开': return '🌐';
case '私有': return '🔒';
case '团队共享': return '👥';
default: return '🔒';
}
};
if (!model) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> - {model.name}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 基本信息 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Brain className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</h4>
<div className="space-y-4">
{/* 模型名称 - 大字体显示 */}
<div className="text-center">
<h3 className="text-2xl font-bold text-foreground mb-2">{model.name}</h3>
<Badge variant="outline" className="text-sm">
<GitBranch className="w-3 h-3 mr-1" />
{model.version}
</Badge>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs"></Label>
<p className="mt-1">
<Badge variant="outline" className="font-light">{model.type}</Badge>
</p>
</div>
<div>
<Label className="text-xs"></Label>
<p className="mt-1">{model.format}</p>
</div>
</div>
<div>
<Label className="text-xs"></Label>
<p className="mt-1 text-sm text-muted-foreground">{model.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs">访</Label>
<p className="mt-1 flex items-center gap-2">
<span>{getAccessLevelIcon(model.accessLevel)}</span>
<span className="text-sm">{model.accessLevel}</span>
</p>
</div>
<div>
<Label className="text-xs"></Label>
<div className="mt-1 flex flex-wrap gap-2">
{model.tags.map((tag, idx) => (
<Badge key={idx} variant="outline" className="text-xs font-light">{tag}</Badge>
))}
</div>
</div>
</div>
</div>
</Card>
{/* 性能指标 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-green-600 dark:text-green-400" />
</h4>
<div className="grid grid-cols-4 gap-4">
<div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
<p className="text-xs text-muted-foreground"></p>
<p className="text-2xl text-green-600 dark:text-green-400 mt-1">{model.accuracy}%</p>
</div>
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<p className="text-xs text-muted-foreground"></p>
<p className="text-2xl text-blue-600 dark:text-blue-400 mt-1">{model.inferenceTime}ms</p>
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1"></p>
</div>
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
<p className="text-xs text-muted-foreground"></p>
<p className="text-2xl text-purple-600 dark:text-purple-400 mt-1">{model.requestCount.toLocaleString()}</p>
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1"></p>
</div>
<div className="text-center p-4 bg-orange-50 dark:bg-orange-950 rounded-lg">
<p className="text-xs text-muted-foreground"></p>
<p className="text-2xl text-orange-600 dark:text-orange-400 mt-1">{model.successRate}%</p>
</div>
</div>
</Card>
{/* API端点信息 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Link className="w-4 h-4 text-blue-600 dark:text-blue-400" />
API端点
</h4>
<div className="space-y-3">
<div>
<Label className="text-xs"></Label>
<div className="mt-2 flex items-center gap-2">
<code className="flex-1 bg-gray-900 dark:bg-gray-950 text-green-400 px-4 py-2 rounded text-sm font-mono">
{model.endpoint}
</code>
<Button size="sm" variant="outline" onClick={() => handleCopyEndpoint(model.endpoint)}>
</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs"></Label>
<p className="mt-1 text-sm">POST</p>
</div>
<div>
<Label className="text-xs">Content-Type</Label>
<p className="mt-1 text-sm">application/json</p>
</div>
</div>
</div>
</Card>
{/* 依赖包列表 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Package className="w-4 h-4 text-purple-600 dark:text-purple-400" />
({model.dependencies.length})
</h4>
<div className="space-y-2">
{model.dependencies.map((dep, idx) => (
<div key={idx} className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded text-sm">
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" />
<code className="font-mono">{dep}</code>
</div>
))}
</div>
</Card>
{/* 调用示例 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Terminal className="w-4 h-4 text-green-600 dark:text-green-400" />
API调用示例
</h4>
<div className="bg-gray-900 dark:bg-gray-950 text-green-400 p-4 rounded-lg font-mono text-sm overflow-x-auto">
<pre>{`# Python调用示例
import requests
url = "${model.endpoint}"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_KEY"
}
payload = {
"data": [
[25.3, 65.2, 45820, 3.2, 1013.2, 18.5, 45.3, 2.3]
]
}
response = requests.post(url, json=payload, headers=headers)
result = response.json()
print(f"预测结果: {result['prediction']}")
print(f"置信度: {result['confidence']}%")`}</pre>
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="outline" onClick={handleTestModel}>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,148 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { toast } from 'sonner';
import { CheckCircle, Upload } from 'lucide-react';
interface ModelService {
id: string;
name: string;
version: string;
type: string;
format: string;
description: string;
accessLevel: string;
tags: string[];
dependencies: string[];
}
interface ModelEditDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
}
export function ModelEditDialog({ open, onOpenChange, model }: ModelEditDialogProps) {
const handleSaveEdit = () => {
toast.success('模型信息已更新');
onOpenChange(false);
};
if (!model) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> - {model.name}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input defaultValue={model.name} />
</div>
<div>
<Label></Label>
<Input defaultValue={model.version} />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Select defaultValue={model.type}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="作物生长预测"></SelectItem>
<SelectItem value="病虫害识别"></SelectItem>
<SelectItem value="产量预估"></SelectItem>
<SelectItem value="土壤分析"></SelectItem>
<SelectItem value="灌溉优化"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select defaultValue={model.format}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ONNX">ONNX</SelectItem>
<SelectItem value="TensorFlow">TensorFlow</SelectItem>
<SelectItem value="PyTorch">PyTorch</SelectItem>
<SelectItem value="Scikit-learn">Scikit-learn</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label></Label>
<Textarea defaultValue={model.description} rows={3} placeholder="描述模型的功能、适用场景等..." />
</div>
<div>
<Label></Label>
<div className="border-2 border-dashed rounded-lg p-6 text-center">
<Upload className="w-8 h-8 mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground mb-1">
</p>
<p className="text-xs text-muted-foreground">
.onnx, .h5, .pb, .pt
</p>
</div>
</div>
<div>
<Label></Label>
<Textarea defaultValue={model.dependencies.join('\n')} rows={3} placeholder="每行一个依赖tensorflow==2.13.0" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>访</Label>
<Select defaultValue={model.accessLevel}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="公开"></SelectItem>
<SelectItem value="私有"></SelectItem>
<SelectItem value="团队共享"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Input defaultValue={model.tags.join(', ')} placeholder="用逗号分隔,如:深度学习,CNN" />
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600" onClick={handleSaveEdit}>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,270 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { toast } from 'sonner';
import {
Activity,
Zap,
Server,
Cpu,
BarChart3,
Eye,
} from 'lucide-react';
interface PerformanceMetrics {
avgResponseTime: number;
p95ResponseTime: number;
p99ResponseTime: number;
qps: number;
errorRate: number;
cpuUsage: number;
memoryUsage: number;
}
interface ModelService {
id: string;
name: string;
}
interface PerformanceTuneDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
performanceMetrics: PerformanceMetrics;
}
export function PerformanceTuneDialog({ open, onOpenChange, model, performanceMetrics }: PerformanceTuneDialogProps) {
const handleApplyTuning = () => {
toast.success('性能优化配置已应用');
onOpenChange(false);
};
if (!model) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> - {model.name}</DialogTitle>
<DialogDescription>
使
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 当前性能指标 */}
<Card className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-950 dark:to-purple-950">
<h4 className="mb-4 flex items-center gap-2">
<Activity className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</h4>
<div className="grid grid-cols-4 gap-4">
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
<p className="text-xs text-muted-foreground"></p>
<p className="text-2xl text-blue-600 dark:text-blue-400 mt-1">{performanceMetrics.avgResponseTime}ms</p>
</div>
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
<p className="text-xs text-muted-foreground">QPS</p>
<p className="text-2xl text-green-600 dark:text-green-400 mt-1">{performanceMetrics.qps}</p>
</div>
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
<p className="text-xs text-muted-foreground">CPU使用率</p>
<p className="text-2xl text-orange-600 dark:text-orange-400 mt-1">{performanceMetrics.cpuUsage}%</p>
</div>
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
<p className="text-xs text-muted-foreground">使</p>
<p className="text-2xl text-purple-600 dark:text-purple-400 mt-1">{performanceMetrics.memoryUsage}%</p>
</div>
</div>
</Card>
{/* 负载均衡配置 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Zap className="w-4 h-4 text-orange-600 dark:text-orange-400" />
</h4>
<div className="space-y-4">
<div>
<Label></Label>
<Select defaultValue="round-robin">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="round-robin"> (Round Robin)</SelectItem>
<SelectItem value="least-connections"> (Least Connections)</SelectItem>
<SelectItem value="ip-hash">IP哈希 (IP Hash)</SelectItem>
<SelectItem value="weighted"> (Weighted)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input type="number" defaultValue="300" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="30" />
</div>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
<Switch defaultChecked />
</div>
</div>
</Card>
{/* 缓存配置 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Server className="w-4 h-4 text-purple-600 dark:text-purple-400" />
</h4>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-purple-50 dark:bg-purple-950 rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
<Switch defaultChecked />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Select defaultValue="lru">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="lru">LRU (使)</SelectItem>
<SelectItem value="lfu">LFU (使)</SelectItem>
<SelectItem value="fifo">FIFO ()</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label> (MB)</Label>
<Input type="number" defaultValue="1024" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="3600" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="10000" />
</div>
</div>
</div>
</Card>
{/* 并发控制 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Cpu className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input type="number" defaultValue="100" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="10" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="1000" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="60" />
</div>
</div>
</Card>
{/* 资源限制 */}
<Card className="p-4">
<h4 className="mb-4"></h4>
<div className="grid grid-cols-2 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm"></span>
<span className="font-medium">8GB</span>
</div>
<Progress value={performanceMetrics.memoryUsage} className="h-2" />
<p className="text-xs text-muted-foreground mt-1">使: {performanceMetrics.memoryUsage}%</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm">CPU限制</span>
<span className="font-medium">4</span>
</div>
<Progress value={performanceMetrics.cpuUsage} className="h-2" />
<p className="text-xs text-muted-foreground mt-1">使: {performanceMetrics.cpuUsage}%</p>
</div>
</div>
</Card>
{/* 性能测试 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-green-600 dark:text-green-400" />
</h4>
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
</p>
<div className="grid grid-cols-3 gap-4">
<div>
<Label></Label>
<Input type="number" defaultValue="50" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="5" />
</div>
<div>
<Label>QPS</Label>
<Input type="number" defaultValue="100" />
</div>
</div>
<Button variant="outline" className="w-full">
<Activity className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="outline">
<Eye className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600" onClick={handleApplyTuning}>
<Zap className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,194 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Card } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { toast } from 'sonner';
import {
Shield,
Unlock,
Users,
Lock,
Gauge,
} from 'lucide-react';
interface ModelService {
id: string;
name: string;
accessLevel: string;
}
interface PermissionManageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
}
export function PermissionManageDialog({ open, onOpenChange, model }: PermissionManageDialogProps) {
const handleSavePermission = () => {
toast.success('权限设置已保存');
onOpenChange(false);
};
if (!model) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> - {model.name}</DialogTitle>
<DialogDescription>
访使
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 访问级别 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Shield className="w-4 h-4 text-blue-600 dark:text-blue-400" />
访
</h4>
<div className="space-y-3">
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer">
<div className="flex items-center gap-3">
<Unlock className="w-5 h-5 text-green-600 dark:text-green-400" />
<div>
<div className="font-medium">访</div>
<div className="text-xs text-muted-foreground">访</div>
</div>
</div>
<input
type="radio"
name="access"
defaultChecked={model.accessLevel === '公开'}
/>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer">
<div className="flex items-center gap-3">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground">访</div>
</div>
</div>
<input
type="radio"
name="access"
defaultChecked={model.accessLevel === '团队共享'}
/>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer">
<div className="flex items-center gap-3">
<Lock className="w-5 h-5 text-red-600 dark:text-red-400" />
<div>
<div className="font-medium">访</div>
<div className="text-xs text-muted-foreground">访</div>
</div>
</div>
<input
type="radio"
name="access"
defaultChecked={model.accessLevel === '私有'}
/>
</div>
</div>
</Card>
{/* API限流 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Gauge className="w-4 h-4 text-orange-600 dark:text-orange-400" />
API限流配置
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input type="number" defaultValue="100" placeholder="0表示无限制" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="10000" placeholder="0表示无限制" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="32" />
</div>
<div>
<Label></Label>
<Input type="number" defaultValue="10" />
</div>
</div>
</Card>
{/* IP白名单 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Shield className="w-4 h-4 text-green-600 dark:text-green-400" />
IP白名单
</h4>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div>
<div className="font-medium">IP白名单</div>
<div className="text-xs text-muted-foreground">IP访问</div>
</div>
<Switch />
</div>
<div>
<Label>IP地址列表</Label>
<Textarea
placeholder="每行一个IP地址或CIDR192.168.1.1 或 10.0.0.0/8"
rows={4}
/>
</div>
</div>
</Card>
{/* 访问令牌管理 */}
<Card className="p-4">
<h4 className="mb-4">访</h4>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950 rounded-lg">
<div>
<div className="font-medium">API密钥认证</div>
<div className="text-xs text-muted-foreground">API调用需要有效的密钥</div>
</div>
<Switch defaultChecked />
</div>
<div className="space-y-2">
<Label>API密钥</Label>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-900 rounded text-sm">
<code className="font-mono text-xs">sk-1234567890abcdef...</code>
<div className="flex gap-1">
<Button size="sm" variant="ghost"></Button>
<Button size="sm" variant="ghost"></Button>
</div>
</div>
</div>
<Button size="sm" variant="outline" className="w-full">
</Button>
</div>
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600" onClick={handleSavePermission}>
<Shield className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,89 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { Server } from 'lucide-react';
interface ServiceConfigDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function ServiceConfigDialog({ open, onOpenChange }: ServiceConfigDialogProps) {
const handleSaveConfig = () => {
toast.success('配置已保存');
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input type="number" placeholder="10" />
</div>
<div>
<Label></Label>
<Input type="number" placeholder="30" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input type="number" placeholder="3" />
</div>
<div>
<Label></Label>
<Input type="number" placeholder="1" />
</div>
</div>
<div className="flex items-center justify-between p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between p-4 bg-green-50 dark:bg-green-950 rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
<Switch defaultChecked />
</div>
<div>
<Label></Label>
<Textarea placeholder="KEY=VALUE每行一个" rows={3} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button className="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600" onClick={handleSaveConfig}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,192 @@
'use client';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { toast } from 'sonner';
import {
GitBranch,
Plus,
CheckCircle,
Download,
RefreshCw,
BarChart3,
} from 'lucide-react';
import {
LineChart as ReLineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
interface ModelService {
id: string;
name: string;
version: string;
lastUpdateTime: string;
}
interface VersionManageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
}
export function VersionManageDialog({ open, onOpenChange, model }: VersionManageDialogProps) {
const handleSwitchVersion = (version: string) => {
toast.success(`已切换到版本 ${version}`);
};
const handleDownloadVersion = (version: string) => {
toast.success(`开始下载版本 ${version}`);
};
if (!model) return null;
// 模拟版本数据
const versions = [
{ version: 'v2.3.1', date: '2024-10-10', accuracy: 94.5, inference: 120, status: '当前', desc: '优化推理性能,提升准确率' },
{ version: 'v2.3.0', date: '2024-09-15', accuracy: 93.8, inference: 135, status: '已归档', desc: '增加新特征,改进模型结构' },
{ version: 'v2.2.0', date: '2024-08-20', accuracy: 92.5, inference: 145, status: '已归档', desc: '数据集扩充,重新训练' },
{ version: 'v2.1.0', date: '2024-07-10', accuracy: 91.2, inference: 150, status: '已归档', desc: '修复已知问题,提升稳定性' },
];
// 版本性能对比数据
const performanceData = [
{ version: 'v2.1.0', 准确率: 91.2, 推理时间: 150 },
{ version: 'v2.2.0', 准确率: 92.5, 推理时间: 145 },
{ version: 'v2.3.0', 准确率: 93.8, 推理时间: 135 },
{ version: 'v2.3.1', 准确率: 94.5, 推理时间: 120 },
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> - {model.name}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 当前版本 */}
<Card className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200 dark:from-blue-950 dark:to-purple-950 dark:border-blue-800">
<div className="flex items-center justify-between">
<div>
<h4 className="flex items-center gap-2">
<GitBranch className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</h4>
<p className="text-2xl mt-2 font-mono">{model.version}</p>
<p className="text-xs text-muted-foreground mt-1">
: {model.lastUpdateTime}
</p>
</div>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"></Badge>
</div>
</Card>
{/* 版本列表 */}
<Card>
<div className="p-4 border-b">
<div className="flex items-center justify-between">
<h4></h4>
<Button size="sm" className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600">
<Plus className="w-3 h-3 mr-1" />
</Button>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{versions.map((ver) => (
<TableRow key={ver.version}>
<TableCell>
<div className="flex items-center gap-2">
{ver.status === '当前' && <CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />}
<code className="font-mono">{ver.version}</code>
</div>
</TableCell>
<TableCell className="text-sm">{ver.date}</TableCell>
<TableCell>
<span className="text-green-600 dark:text-green-400">{ver.accuracy}%</span>
</TableCell>
<TableCell>{ver.inference}ms</TableCell>
<TableCell>
<Badge className={ver.status === '当前' ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'}>
{ver.status}
</Badge>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{ver.desc}
</TableCell>
<TableCell>
<div className="flex gap-2">
{ver.status !== '当前' && (
<Button size="sm" variant="outline" onClick={() => handleSwitchVersion(ver.version)}>
<RefreshCw className="w-3 h-3" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => handleDownloadVersion(ver.version)}>
<Download className="w-3 h-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
{/* 版本对比 */}
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-purple-600 dark:text-purple-400" />
</h4>
<ResponsiveContainer width="100%" height={250}>
<ReLineChart data={performanceData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="version" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<RechartsTooltip />
<Legend />
<Line yAxisId="left" type="monotone" dataKey="准确率" stroke="#10b981" strokeWidth={2} />
<Line yAxisId="right" type="monotone" dataKey="推理时间" stroke="#3b82f6" strokeWidth={2} />
</ReLineChart>
</ResponsiveContainer>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600">
<Download className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,759 @@
/**
* filekorolheader: 模型接入页面 - AI模型统一接入管理平台
* 功能:模型服务注册、接入步骤说明、格式支持展示、快速接入示例、模型管理
* 路径:/ai-crop-model/model-integration/access
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式支持暗色主题
*/
'use client';
import { useState, useReducer } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Progress } from '@/components/ui/progress';
import { Switch } from '@/components/ui/switch';
import { copyToClipboard } from '@/lib/clipboard';
import {
Brain,
Plus,
Search,
Download,
Upload,
RefreshCw,
CheckCircle,
XCircle,
AlertCircle,
PlayCircle,
PauseCircle,
Settings,
Eye,
Edit,
Trash2,
Link,
Cpu,
Zap,
Shield,
BarChart3,
Activity,
Clock,
Package,
Server,
Gauge,
TrendingUp,
Users,
Lock,
Unlock,
Copy,
GitBranch,
Code,
Terminal,
RotateCw,
} from 'lucide-react';
import { toast } from 'sonner';
// 导入弹窗组件
import { ServiceConfigDialog } from './components/ServiceConfigDialog';
import { ModelDetailDialog } from './components/ModelDetailDialog';
import { VersionManageDialog } from './components/VersionManageDialog';
import { DeployConfigDialog } from './components/DeployConfigDialog';
import { ModelEditDialog } from './components/ModelEditDialog';
import { PermissionManageDialog } from './components/PermissionManageDialog';
import { DependencyManageDialog } from './components/DependencyManageDialog';
import { PerformanceTuneDialog } from './components/PerformanceTuneDialog';
// 类型定义
type ModelType = '作物生长预测' | '病虫害识别' | '产量预估' | '土壤分析' | '灌溉优化' | '其他';
type ModelFormat = 'ONNX' | 'TensorFlow' | 'PyTorch' | 'Scikit-learn' | 'H5' | 'SavedModel';
type ModelStatus = '运行中' | '已停止' | '部署中' | '故障' | '维护中';
type AccessLevel = '公开' | '私有' | '团队共享';
interface ModelService {
id: string;
name: string;
version: string;
type: ModelType;
format: ModelFormat;
description: string;
author: string;
createTime: string;
lastUpdateTime: string;
status: ModelStatus;
endpoint: string;
accessLevel: AccessLevel;
tags: string[];
accuracy?: number;
inferenceTime?: number;
requestCount: number;
successRate: number;
dependencies: string[];
}
interface PerformanceMetrics {
avgResponseTime: number;
p95ResponseTime: number;
p99ResponseTime: number;
qps: number;
errorRate: number;
cpuUsage: number;
memoryUsage: number;
}
interface ModelState {
modelServices: ModelService[];
selectedModel: ModelService | null;
showModelDialog: boolean;
showConfigDialog: boolean;
showDetailDialog: boolean;
showVersionDialog: boolean;
showDeployDialog: boolean;
showEditDialog: boolean;
showPermissionDialog: boolean;
showDependencyDialog: boolean;
showPerformanceDialog: boolean;
performanceMetrics: PerformanceMetrics;
}
type ModelAction =
| { type: 'SET_MODEL_SERVICES'; payload: ModelService[] }
| { type: 'SET_SELECTED_MODEL'; payload: ModelService | null }
| { type: 'TOGGLE_DIALOG'; payload: keyof Pick<ModelState, 'showModelDialog' | 'showConfigDialog' | 'showDetailDialog' | 'showVersionDialog' | 'showDeployDialog' | 'showEditDialog' | 'showPermissionDialog' | 'showDependencyDialog' | 'showPerformanceDialog'> }
| { type: 'UPDATE_MODEL_STATUS'; payload: { id: string; status: ModelStatus } };
// 初始状态
const initialState: ModelState = {
modelServices: [
{
id: 'model-1',
name: '番茄生长预测模型',
version: 'v2.3.1',
type: '作物生长预测',
format: 'TensorFlow',
description: '基于深度学习的番茄生长周期预测模型,综合温湿度、光照等因素',
author: '农业AI研究院',
createTime: '2024-03-15',
lastUpdateTime: '2024-10-10',
status: '运行中',
endpoint: 'https://api.farm-ai.com/v1/tomato-growth',
accessLevel: '团队共享',
tags: ['作物生长', '番茄', '深度学习'],
accuracy: 94.5,
inferenceTime: 120,
requestCount: 15680,
successRate: 99.2,
dependencies: ['tensorflow==2.13.0', 'numpy==1.24.0', 'pandas==2.0.0'],
},
{
id: 'model-2',
name: '病虫害智能识别',
version: 'v1.8.0',
type: '病虫害识别',
format: 'PyTorch',
description: '基于卷积神经网络的作物病虫害图像识别模型支持20+种常见病虫害',
author: '植保技术团队',
createTime: '2024-05-20',
lastUpdateTime: '2024-10-12',
status: '运行中',
endpoint: 'https://api.farm-ai.com/v1/pest-detection',
accessLevel: '公开',
tags: ['病虫害', '图像识别', 'CNN'],
accuracy: 96.8,
inferenceTime: 85,
requestCount: 28950,
successRate: 98.7,
dependencies: ['torch==2.0.1', 'torchvision==0.15.0', 'opencv-python==4.8.0'],
},
{
id: 'model-3',
name: '产量预估分析',
version: 'v3.1.2',
type: '产量预估',
format: 'ONNX',
description: '综合历史数据和实时监测的作物产量预估模型',
author: '数据分析中心',
createTime: '2024-01-10',
lastUpdateTime: '2024-09-28',
status: '运行中',
endpoint: 'https://api.farm-ai.com/v1/yield-prediction',
accessLevel: '私有',
tags: ['产量预估', 'LSTM', '时间序列'],
accuracy: 92.3,
inferenceTime: 150,
requestCount: 8520,
successRate: 99.5,
dependencies: ['onnxruntime==1.15.0', 'scikit-learn==1.3.0'],
},
{
id: 'model-4',
name: '土壤养分分析',
version: 'v2.0.0',
type: '土壤分析',
format: 'Scikit-learn',
description: '基于机器学习的土壤养分含量预测与分析模型',
author: '土壤实验室',
createTime: '2024-06-01',
lastUpdateTime: '2024-10-05',
status: '已停止',
endpoint: 'https://api.farm-ai.com/v1/soil-analysis',
accessLevel: '团队共享',
tags: ['土壤分析', '机器学习'],
accuracy: 89.7,
inferenceTime: 95,
requestCount: 4250,
successRate: 97.8,
dependencies: ['scikit-learn==1.3.0', 'xgboost==2.0.0'],
},
],
selectedModel: null,
showModelDialog: false,
showConfigDialog: false,
showDetailDialog: false,
showVersionDialog: false,
showDeployDialog: false,
showEditDialog: false,
showPermissionDialog: false,
showDependencyDialog: false,
showPerformanceDialog: false,
performanceMetrics: {
avgResponseTime: 115,
p95ResponseTime: 280,
p99ResponseTime: 450,
qps: 45.6,
errorRate: 0.8,
cpuUsage: 42.5,
memoryUsage: 68.3,
},
};
// Reducer
function modelReducer(state: ModelState, action: ModelAction): ModelState {
switch (action.type) {
case 'SET_MODEL_SERVICES':
return { ...state, modelServices: action.payload };
case 'SET_SELECTED_MODEL':
return { ...state, selectedModel: action.payload };
case 'TOGGLE_DIALOG':
return { ...state, [action.payload]: !state[action.payload] };
case 'UPDATE_MODEL_STATUS':
return {
...state,
modelServices: state.modelServices.map(model =>
model.id === action.payload.id
? { ...model, status: action.payload.status }
: model
),
};
default:
return state;
}
}
export default function ModelAccessPage() {
const [state, dispatch] = useReducer(modelReducer, initialState);
// 计算统计数据
const totalModels = state.modelServices.length;
const runningModels = state.modelServices.filter(m => m.status === '运行中').length;
const stoppedModels = state.modelServices.filter(m => m.status === '已停止').length;
const avgAccuracy = state.modelServices.reduce((sum, m) => sum + (m.accuracy || 0), 0) / totalModels;
// 模型类型分布数据
const modelTypeDistribution = [
{ name: '作物生长预测', value: 3, color: '#10b981' },
{ name: '病虫害识别', value: 2, color: '#3b82f6' },
{ name: '产量预估', value: 2, color: '#f59e0b' },
{ name: '土壤分析', value: 1, color: '#8b5cf6' },
{ name: '灌溉优化', value: 1, color: '#ec4899' },
];
// 模型调用趋势数据
const modelCallTrend = [
{ time: '10:00', 调用次数: 120, 成功率: 98.5 },
{ time: '11:00', 调用次数: 156, 成功率: 99.1 },
{ time: '12:00', 调用次数: 142, 成功率: 98.8 },
{ time: '13:00', 调用次数: 178, 成功率: 99.3 },
{ time: '14:00', 调用次数: 195, 成功率: 99.0 },
];
// 工具函数
const getStatusColor = (status: ModelStatus) => {
switch (status) {
case '运行中': return 'bg-success-muted text-success-muted-foreground';
case '已停止': return 'bg-muted text-muted-foreground';
case '部署中': return 'bg-info-muted text-info-muted-foreground';
case '故障': return 'bg-error-muted text-error-muted-foreground';
case '维护中': return 'bg-warning-muted text-warning-muted-foreground';
default: return 'bg-muted text-muted-foreground';
}
};
const getStatusIcon = (status: ModelStatus) => {
switch (status) {
case '运行中': return <PlayCircle className="w-4 h-4 text-success" />;
case '已停止': return <PauseCircle className="w-4 h-4 text-muted-foreground" />;
case '部署中': return <RefreshCw className="w-4 h-4 text-info animate-spin" />;
case '故障': return <XCircle className="w-4 h-4 text-error" />;
case '维护中': return <AlertCircle className="w-4 h-4 text-warning" />;
default: return <AlertCircle className="w-4 h-4 text-muted-foreground" />;
}
};
const getAccessLevelIcon = (level: AccessLevel) => {
switch (level) {
case '公开': return <Unlock className="w-4 h-4 text-green-600" />;
case '私有': return <Lock className="w-4 h-4 text-red-600" />;
case '团队共享': return <Users className="w-4 h-4 text-blue-600" />;
default: return <Lock className="w-4 h-4 text-gray-600" />;
}
};
// 事件处理函数
const handleToggleDialog = (dialog: keyof Pick<ModelState, 'showModelDialog' | 'showConfigDialog' | 'showDetailDialog' | 'showVersionDialog' | 'showDeployDialog' | 'showEditDialog' | 'showPermissionDialog' | 'showDependencyDialog' | 'showPerformanceDialog'>) => {
dispatch({ type: 'TOGGLE_DIALOG', payload: dialog });
};
const handleSelectModel = (model: ModelService) => {
dispatch({ type: 'SET_SELECTED_MODEL', payload: model });
};
const handleStartModel = (modelName: string) => {
toast.success(`模型"${modelName}"已启动`);
};
const handleStopModel = (modelName: string) => {
toast.success(`模型"${modelName}"已停止`);
};
const handleTestModel = () => {
toast.success('模型测试成功,推理正常');
};
const handleSaveModel = () => {
toast.success('模型服务注册成功');
handleToggleDialog('showModelDialog');
};
const handleCopyEndpoint = async (endpoint: string) => {
const success = await copyToClipboard(endpoint);
if (success) {
toast.success('端点已复制到剪贴板');
} else {
toast.error('复制失败,请重试');
}
};
const handleEditModel = (model: ModelService) => {
handleSelectModel(model);
handleToggleDialog('showEditDialog');
};
const handleViewDetail = (model: ModelService) => {
handleSelectModel(model);
handleToggleDialog('showDetailDialog');
};
const handleViewVersions = (model: ModelService) => {
handleSelectModel(model);
handleToggleDialog('showVersionDialog');
};
const handlePermissionManage = (model: ModelService) => {
handleSelectModel(model);
handleToggleDialog('showPermissionDialog');
};
const handleDependencyManage = (model: ModelService) => {
handleSelectModel(model);
handleToggleDialog('showDependencyDialog');
};
const handlePerformanceTune = (model: ModelService) => {
handleSelectModel(model);
handleToggleDialog('showPerformanceDialog');
};
const handleRedeploy = (model: ModelService) => {
toast.success(`模型"${model.name}"重新部署已启动预计3-5分钟完成`);
};
const handleConfigModel = (model: ModelService) => {
handleSelectModel(model);
handleToggleDialog('showConfigDialog');
};
return (
<div className="space-y-6">
{/* 页面标题 */}
<div>
<h2 className="text-3xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground mt-1">
AI模型统一接入
</p>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-blue-600 dark:text-blue-400">{totalModels}</p>
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1"></p>
</div>
<Brain className="w-12 h-12 text-blue-600 dark:text-blue-400 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-green-600 dark:text-green-400">{runningModels}</p>
<p className="text-xs text-green-600 dark:text-green-400 mt-1"></p>
</div>
<PlayCircle className="w-12 h-12 text-green-600 dark:text-green-400 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-purple-600 dark:text-purple-400">{avgAccuracy.toFixed(1)}%</p>
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1"></p>
</div>
<BarChart3 className="w-12 h-12 text-purple-600 dark:text-purple-400 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">QPS</p>
<p className="mt-2 text-3xl text-orange-600 dark:text-orange-400">{state.performanceMetrics.qps}</p>
<p className="text-xs text-orange-600 dark:text-orange-400 mt-1"></p>
</div>
<Zap className="w-12 h-12 text-orange-600 dark:text-orange-400 opacity-50" />
</div>
</Card>
</div>
{/* 功能说明卡片 */}
<Card className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200 dark:from-blue-950 dark:to-purple-950 dark:border-blue-800">
<div className="flex items-start gap-2">
<Brain className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-900 dark:text-blue-100">
<p className="mb-2"></p>
<ul className="space-y-1 text-xs">
<li> <strong></strong>: </li>
<li> <strong></strong>: AI模型</li>
<li> <strong></strong>: ONNXTensorFlowPyTorch等主流框架</li>
<li> <strong></strong>: </li>
<li> <strong></strong>: </li>
</ul>
</div>
</div>
</Card>
{/* 操作按钮 */}
<div className="flex gap-4">
<Button
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600"
onClick={() => {
dispatch({ type: 'SET_SELECTED_MODEL', payload: null });
handleToggleDialog('showModelDialog');
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 接入步骤说明 */}
<Card className="p-6">
<h4 className="mb-4 flex items-center gap-2">
<Code className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</h4>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mx-auto mb-3">
<Upload className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<h5 className="mb-2">1. </h5>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mx-auto mb-3">
<Settings className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<h5 className="mb-2">2. </h5>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mx-auto mb-3">
<CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<h5 className="mb-2">3. </h5>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="text-center p-4 bg-orange-50 dark:bg-orange-950 rounded-lg">
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center mx-auto mb-3">
<Server className="w-6 h-6 text-orange-600 dark:text-orange-400" />
</div>
<h5 className="mb-2">4. </h5>
<p className="text-xs text-muted-foreground">
API服务接口
</p>
</div>
</div>
</Card>
{/* 模型格式支持 */}
<Card className="p-6">
<h4 className="mb-4 flex items-center gap-2">
<Package className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{['ONNX', 'TensorFlow', 'PyTorch', 'Scikit-learn', 'H5', 'SavedModel'].map((format) => (
<div key={format} className="flex items-center gap-3 p-3 bg-muted dark:bg-muted rounded-lg">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
<div>
<div className="font-medium">{format}</div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
))}
</div>
</Card>
{/* 快速接入示例 */}
<Card className="p-6">
<h4 className="mb-4 flex items-center gap-2">
<Terminal className="w-5 h-5 text-green-600 dark:text-green-400" />
</h4>
<div className="bg-foreground text-green-400 p-4 rounded-lg font-mono text-sm overflow-x-auto">
<pre>{`# 使用Python SDK快速注册模型
from farm_ai_sdk import ModelRegistry
# 1. 初始化注册器
registry = ModelRegistry(api_key="your_api_key")
# 2. 注册模型
model = registry.register(
name="番茄生长预测模型",
model_path="./tomato_growth_v2.onnx",
model_type="作物生长预测",
version="v2.3.1",
metadata={
"input_shape": "(batch, 10, 8)",
"output_shape": "(batch, 1)",
"framework": "ONNX",
}
)
# 3. 部署模型
deployment = registry.deploy(
model_id=model.id,
replicas=3,
enable_autoscaling=True
)
print(f"模型已部署: {deployment.endpoint}")`}</pre>
</div>
</Card>
{/* 模型注册对话框 */}
<Dialog open={state.showModelDialog} onOpenChange={() => handleToggleDialog('showModelDialog')}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>AI模型</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input placeholder="输入模型名称" />
</div>
<div>
<Label></Label>
<Input placeholder="v1.0.0" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="选择模型类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="作物生长预测"></SelectItem>
<SelectItem value="病虫害识别"></SelectItem>
<SelectItem value="产量预估"></SelectItem>
<SelectItem value="土壤分析"></SelectItem>
<SelectItem value="灌溉优化"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="选择模型格式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ONNX">ONNX</SelectItem>
<SelectItem value="TensorFlow">TensorFlow</SelectItem>
<SelectItem value="PyTorch">PyTorch</SelectItem>
<SelectItem value="Scikit-learn">Scikit-learn</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label></Label>
<Textarea placeholder="描述模型的功能、适用场景等..." rows={3} />
</div>
<div>
<Label></Label>
<div className="border-2 border-dashed rounded-lg p-6 text-center">
<Upload className="w-8 h-8 mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground mb-1">
</p>
<p className="text-xs text-muted-foreground">
.onnx, .h5, .pb, .pt
</p>
</div>
</div>
<div>
<Label></Label>
<Textarea placeholder="每行一个依赖tensorflow==2.13.0" rows={3} />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>访</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="选择访问权限" />
</SelectTrigger>
<SelectContent>
<SelectItem value="公开"></SelectItem>
<SelectItem value="私有"></SelectItem>
<SelectItem value="团队共享"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Input placeholder="用逗号分隔,如:深度学习,CNN" />
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => handleToggleDialog('showModelDialog')}>
</Button>
<Button variant="outline" onClick={handleTestModel}>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600" onClick={handleSaveModel}>
<Server className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 所有弹窗组件 */}
<ServiceConfigDialog
open={state.showConfigDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showConfigDialog');
}}
/>
<ModelDetailDialog
open={state.showDetailDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showDetailDialog');
}}
model={state.selectedModel}
/>
<VersionManageDialog
open={state.showVersionDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showVersionDialog');
}}
model={state.selectedModel}
/>
<DeployConfigDialog
open={state.showDeployDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showDeployDialog');
}}
/>
<ModelEditDialog
open={state.showEditDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showEditDialog');
}}
model={state.selectedModel}
/>
<PermissionManageDialog
open={state.showPermissionDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showPermissionDialog');
}}
model={state.selectedModel}
/>
<DependencyManageDialog
open={state.showDependencyDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showDependencyDialog');
}}
model={state.selectedModel}
/>
<PerformanceTuneDialog
open={state.showPerformanceDialog}
onOpenChange={(open) => {
if (!open) handleToggleDialog('showPerformanceDialog');
}}
model={state.selectedModel}
performanceMetrics={state.performanceMetrics}
/>
</div>
);
}

View File

@@ -0,0 +1,276 @@
/**
* filekorolheader: 模型注册对话框组件 - 新增AI模型注册
* 功能:模型基本信息填写、文件上传、权限配置、依赖管理
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { useState } from 'react';
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 {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter
} from '@/components/ui/dialog';
import { Upload, Save } from 'lucide-react';
import { toast } from 'sonner';
interface AddModelDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
dispatch: React.Dispatch<any>;
}
export function AddModelDialog({ open, onOpenChange, dispatch }: AddModelDialogProps) {
const [formData, setFormData] = useState({
name: '',
version: '',
type: '',
format: '',
description: '',
access: '',
tags: '',
dependencies: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSubmit = async () => {
// 验证必填项
if (!formData.name.trim()) {
toast.error('请输入模型名称');
return;
}
if (!formData.version.trim()) {
toast.error('请输入模型版本');
return;
}
if (!formData.type) {
toast.error('请选择模型类型');
return;
}
if (!formData.format) {
toast.error('请选择模型格式');
return;
}
setIsSubmitting(true);
try {
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 2000));
// 创建新模型对象
const newModel = {
id: Date.now().toString(),
name: formData.name,
type: formData.type,
version: formData.version,
status: 'active',
description: formData.description,
apiEndpoint: `https://api.smart-crop.com/v2/models/${formData.name.toLowerCase().replace(/\s+/g, '-')}`,
parameters: {
confidence_threshold: 0.85,
max_detections: 10
},
metrics: {
total_calls: 0,
avg_response_time: 200,
success_rate: 100,
accuracy: 95.0,
last_called: new Date().toISOString(),
qps: 0
},
tags: formData.tags ? formData.tags.split(',').map(tag => tag.trim()) : [],
visibility: formData.access === '公开' ? 'public' : formData.access === '团队共享' ? 'team' : 'private',
team: 'AI算法团队',
concurrency: 100,
timeout: 30000,
retryCount: 3,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// 添加模型
dispatch({ type: 'ADD_MODEL', payload: newModel });
dispatch({ type: 'SET_ADD_MODEL_DIALOG', payload: false });
// 重置表单
setFormData({
name: '',
version: '',
type: '',
format: '',
description: '',
access: '',
tags: '',
dependencies: ''
});
toast.success('模型注册成功!');
} catch (error) {
toast.error('注册模型失败,请重试');
console.error('Add model error:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>AI模型</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input
placeholder="输入模型名称"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
</div>
<div>
<Label></Label>
<Input
placeholder="v1.0.0"
value={formData.version}
onChange={(e) => handleInputChange('version', e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Select value={formData.type} onValueChange={(value) => handleInputChange('type', value)}>
<SelectTrigger>
<SelectValue placeholder="选择模型类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pest_identification"></SelectItem>
<SelectItem value="growth_status"></SelectItem>
<SelectItem value="yield_prediction"></SelectItem>
<SelectItem value="soil_analysis"></SelectItem>
<SelectItem value="irrigation_optimization"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select value={formData.format} onValueChange={(value) => handleInputChange('format', value)}>
<SelectTrigger>
<SelectValue placeholder="选择模型格式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ONNX">ONNX</SelectItem>
<SelectItem value="TensorFlow">TensorFlow</SelectItem>
<SelectItem value="PyTorch">PyTorch</SelectItem>
<SelectItem value="Scikit-learn">Scikit-learn</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label></Label>
<Textarea
placeholder="描述模型的功能、适用场景等..."
rows={3}
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
/>
</div>
<div>
<Label></Label>
<div className="border-2 border-dashed rounded-lg p-6 text-center">
<Upload className="w-8 h-8 mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground mb-1">
</p>
<p className="text-xs text-muted-foreground">
.onnx, .h5, .pb, .pt
</p>
</div>
</div>
<div>
<Label></Label>
<Textarea
placeholder="每行一个依赖tensorflow==2.13.0"
rows={3}
value={formData.dependencies}
onChange={(e) => handleInputChange('dependencies', e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>访</Label>
<Select value={formData.access} onValueChange={(value) => handleInputChange('access', value)}>
<SelectTrigger>
<SelectValue placeholder="选择访问权限" />
</SelectTrigger>
<SelectContent>
<SelectItem value="公开"></SelectItem>
<SelectItem value="私有"></SelectItem>
<SelectItem value="团队共享"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Input
placeholder="用逗号分隔,如:深度学习,CNN"
value={formData.tags}
onChange={(e) => handleInputChange('tags', e.target.value)}
/>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? '注册中...' : (
<>
<Save className="h-4 w-4 mr-2" />
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,245 @@
/**
* filekorolheader: 删除模型确认对话框 - 模型服务删除确认界面
* 功能:删除确认、模型信息展示、风险提示、删除处理
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用AlertDialog组件安全删除确认
*/
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
import { ModelService } from '../types';
import { AlertTriangle, Trash2, ExternalLink } from 'lucide-react';
import { toast } from 'sonner';
interface DeleteModelDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
dispatch: React.Dispatch<any>;
}
const getModelTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'image_classification': '图像分类',
'object_detection': '目标检测',
'regression': '回归预测',
'multiclass_classification': '多类分类',
'optimization': '优化算法',
'time_series': '时序分析',
'nlp': '自然语言处理',
'anomaly_detection': '异常检测'
};
return labels[type] || type;
};
const getStatusBadge = (status: string) => {
const statusConfig = {
active: {
label: '运行中',
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
},
inactive: {
label: '已停止',
className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
},
testing: {
label: '测试中',
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
},
error: {
label: '错误',
className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
}
};
const config = statusConfig[status as keyof typeof statusConfig];
return (
<Badge className={config.className}>
{config.label}
</Badge>
);
};
export function DeleteModelDialog({ open, onOpenChange, model, dispatch }: DeleteModelDialogProps) {
const [isDeleting, setIsDeleting] = useState(false);
const handleDelete = async () => {
if (!model) return;
setIsDeleting(true);
try {
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1500));
// 删除模型
dispatch({ type: 'DELETE_MODEL', payload: model.id });
dispatch({ type: 'SET_DELETE_DIALOG', payload: false });
toast.success(`模型 "${model.name}" 已成功删除`);
} catch (error) {
toast.error('删除模型失败,请重试');
console.error('Delete model error:', error);
} finally {
setIsDeleting(false);
}
};
if (!model) return null;
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="max-w-lg">
<AlertDialogHeader>
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900">
<Trash2 className="h-6 w-6 text-red-600 dark:text-red-400" />
</div>
</div>
<div className="flex-1">
<AlertDialogTitle className="text-red-600 dark:text-red-400">
</AlertDialogTitle>
<AlertDialogDescription className="text-left mt-2">
AI模型服务吗
</AlertDialogDescription>
</div>
</div>
</AlertDialogHeader>
<div className="my-6">
{/* 模型信息卡片 */}
<Card className="p-4 bg-muted/30 border-red-200 dark:border-red-800">
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="font-medium text-foreground"></h4>
{getStatusBadge(model.status)}
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium text-foreground">{model.name}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium text-foreground">{getModelTypeLabel(model.type)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-mono text-foreground">{model.version}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">API端点:</span>
<div className="flex items-center space-x-1">
<span className="font-mono text-xs text-foreground max-w-xs truncate">
{model.apiEndpoint}
</span>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => window.open(model.apiEndpoint, '_blank')}
title="访问API"
>
<ExternalLink className="h-3 w-3" />
</Button>
</div>
</div>
</div>
<div className="pt-3 border-t border-border">
<div className="grid grid-cols-3 gap-3 text-center">
<div>
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">
{model.metrics.total_calls.toLocaleString()}
</div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div>
<div className="text-lg font-bold text-green-600 dark:text-green-400">
{model.metrics.accuracy.toFixed(1)}%
</div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div>
<div className="text-lg font-bold text-orange-600 dark:text-orange-400">
{model.metrics.qps.toFixed(1)}
</div>
<div className="text-xs text-muted-foreground">QPS</div>
</div>
</div>
</div>
</div>
</Card>
{/* 删除影响提示 */}
<div className="mt-4 p-3 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-start space-x-2">
<AlertTriangle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
<div className="text-sm text-red-800 dark:text-red-200">
<p className="font-medium mb-1"></p>
<ul className="space-y-1 text-xs">
<li> </li>
<li> API调用服务</li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
{/* 创建时间信息 */}
<div className="mt-3 text-xs text-muted-foreground">
<p>
: {new Date(model.createdAt).toLocaleString('zh-CN')}
{model.updatedAt !== model.createdAt && (
<span className="ml-2">
| : {new Date(model.updatedAt).toLocaleString('zh-CN')}
</span>
)}
</p>
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={isDeleting}
className="bg-red-600 hover:bg-red-700 text-white"
>
{isDeleting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
...
</>
) : (
<>
<Trash2 className="h-4 w-4 mr-2" />
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -0,0 +1,139 @@
/**
* filekorolheader: 模型分析图表组件 - 模型调用趋势和类型分布
* 功能:展示模型调用趋势图表、模型类型分布饼图
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { Card } from '@/components/ui/card';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
Legend,
ResponsiveContainer,
PieChart,
Pie,
Cell,
BarChart,
Bar
} from 'recharts';
import { TrendingUp, PieChart as PieChartIcon } from 'lucide-react';
interface ModelAnalyticsProps {
models: any[];
}
const getModelTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'pest_identification': '病虫害识别',
'growth_status': '生长状态',
'yield_prediction': '产量预测'
};
return labels[type] || type;
};
export function ModelAnalytics({ models }: ModelAnalyticsProps) {
// 模拟调用趋势数据 (最近7天)
const callTrendData = [
{ date: '03-15', calls: 12500, success: 12200, failed: 300 },
{ date: '03-16', calls: 13800, success: 13500, failed: 300 },
{ date: '03-17', calls: 14200, success: 13850, failed: 350 },
{ date: '03-18', calls: 15600, success: 15200, failed: 400 },
{ date: '03-19', calls: 16800, success: 16400, failed: 400 },
{ date: '03-20', calls: 18900, success: 18450, failed: 450 },
{ date: '03-21', calls: 21300, success: 20800, failed: 500 }
];
// 模型类型分布数据
const modelTypeData = models.reduce((acc, model) => {
const existing = acc.find(item => item.type === model.type);
if (existing) {
existing.count += 1;
} else {
acc.push({
type: model.type,
count: 1,
name: getModelTypeLabel(model.type)
});
}
return acc;
}, [] as { type: string; count: number; name: string }[]);
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'];
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 模型调用趋势 */}
<Card className="p-6">
<div className="flex items-center gap-2 mb-4">
<TrendingUp className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<h3 className="text-lg font-semibold text-foreground"></h3>
</div>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={callTrendData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<RechartsTooltip />
<Legend />
<Line
type="monotone"
dataKey="success"
stroke="#10b981"
strokeWidth={2}
name="成功调用"
/>
<Line
type="monotone"
dataKey="failed"
stroke="#ef4444"
strokeWidth={2}
name="失败调用"
/>
<Line
type="monotone"
dataKey="calls"
stroke="#3b82f6"
strokeWidth={2}
name="总调用"
/>
</LineChart>
</ResponsiveContainer>
</Card>
{/* 模型类型分布 */}
<Card className="p-6">
<div className="flex items-center gap-2 mb-4">
<PieChartIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<h3 className="text-lg font-semibold text-foreground"></h3>
</div>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={modelTypeData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="count"
>
{modelTypeData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<RechartsTooltip />
</PieChart>
</ResponsiveContainer>
</Card>
</div>
);
}

View File

@@ -0,0 +1,450 @@
/**
* filekorolheader: 模型配置对话框组件 - 模型编辑与查看界面
* 功能:模型信息编辑、参数配置、查看模式、保存处理
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md支持编辑/查看双模式shadcn语义化样式
*/
'use client';
import { useState, useEffect } from 'react';
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 {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
import { ModelService, ModelType, ModelStatus } from '../types';
import { Edit, Eye, Save, Copy, ExternalLink } from 'lucide-react';
import { toast } from 'sonner';
interface ModelConfigDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: ModelService | null;
viewMode?: boolean;
dispatch: React.Dispatch<any>;
}
const MODEL_TYPES: { value: ModelType; label: string; description: string }[] = [
{
value: 'pest_identification',
label: '病虫害识别',
description: '识别农作物病虫害'
},
{
value: 'growth_status',
label: '生长状态',
description: '分析作物生长阶段'
},
{
value: 'yield_prediction',
label: '产量预测',
description: '预测农作物产量'
}
];
const STATUS_OPTIONS: { value: ModelStatus; label: string; color: string }[] = [
{ value: 'active', label: '运行中', color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
{ value: 'inactive', label: '已停止', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200' },
{ value: 'testing', label: '测试中', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
{ value: 'error', label: '错误', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }
];
export function ModelConfigDialog({ open, onOpenChange, model, viewMode = false, dispatch }: ModelConfigDialogProps) {
const [formData, setFormData] = useState<Partial<ModelService>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (open && model) {
setFormData(model);
}
}, [open, model]);
const handleInputChange = (field: keyof ModelService, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.success('已复制到剪贴板');
};
const validateForm = (): boolean => {
if (!formData.name?.trim()) {
toast.error('请输入模型名称');
return false;
}
if (!formData.description?.trim()) {
toast.error('请输入模型描述');
return false;
}
if (!formData.apiEndpoint?.trim()) {
toast.error('请输入API端点地址');
return false;
}
return true;
};
const handleSubmit = async () => {
if (viewMode) return;
if (!validateForm()) return;
if (!model) return;
setIsSubmitting(true);
try {
// 更新模型对象
const updatedModel: ModelService = {
...model,
...formData,
updatedAt: new Date().toISOString()
} as ModelService;
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 更新模型
dispatch({ type: 'UPDATE_MODEL', payload: updatedModel });
dispatch({ type: 'SET_CONFIG_DIALOG', payload: false });
toast.success('模型更新成功!');
} catch (error) {
toast.error('更新模型失败,请重试');
console.error('Update model error:', error);
} finally {
setIsSubmitting(false);
}
};
if (!model) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center space-x-2">
{viewMode ? (
<Eye className="h-5 w-5 text-blue-500" />
) : (
<Edit className="h-5 w-5 text-orange-500" />
)}
<DialogTitle>
{viewMode ? '查看模型详情' : '编辑模型配置'}
</DialogTitle>
</div>
<DialogDescription>
{viewMode
? '查看模型的详细信息和配置参数'
: '修改模型的基本信息和参数配置'
}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* 模型基本信息 */}
<Card className="p-4 bg-muted/30">
<h4 className="font-medium text-foreground mb-4"></h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => !viewMode && handleInputChange('name', e.target.value)}
disabled={viewMode}
placeholder="模型名称"
/>
</div>
<div className="space-y-2">
<Label htmlFor="type"></Label>
<Select
value={formData.type}
onValueChange={(value: ModelType) => !viewMode && handleInputChange('type', value)}
disabled={viewMode}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{MODEL_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
<div className="flex flex-col items-start">
<span>{type.label}</span>
<span className="text-xs text-muted-foreground">
{type.description}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="version"></Label>
<Input
id="version"
value={formData.version || ''}
onChange={(e) => !viewMode && handleInputChange('version', e.target.value)}
disabled={viewMode}
placeholder="例如1.0.0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="status"></Label>
<div className="flex items-center space-x-2">
<Select
value={formData.status}
onValueChange={(value: ModelStatus) => !viewMode && handleInputChange('status', value)}
disabled={viewMode}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((status) => (
<SelectItem key={status.value} value={status.value}>
<Badge className={status.color}>
{status.label}
</Badge>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="apiEndpoint">API端点地址</Label>
<div className="flex space-x-2">
<Input
id="apiEndpoint"
value={formData.apiEndpoint || ''}
onChange={(e) => !viewMode && handleInputChange('apiEndpoint', e.target.value)}
disabled={viewMode}
placeholder="https://api.example.com/v1/models/..."
className="flex-1"
/>
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(formData.apiEndpoint || '')}
title="复制API地址"
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => window.open(formData.apiEndpoint, '_blank')}
title="访问API"
>
<ExternalLink className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description || ''}
onChange={(e) => !viewMode && handleInputChange('description', e.target.value)}
disabled={viewMode}
placeholder="描述模型的功能、应用场景、技术特点等..."
rows={3}
/>
</div>
</div>
</Card>
{/* 服务配置 */}
<Card className="p-4 bg-muted/30">
<h4 className="font-medium text-foreground mb-4"></h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="concurrency"></Label>
<Input
id="concurrency"
type="number"
value={formData.concurrency || ''}
onChange={(e) => !viewMode && handleInputChange('concurrency', e.target.value)}
disabled={viewMode}
placeholder="100"
/>
</div>
<div className="space-y-2">
<Label htmlFor="timeout">(ms)</Label>
<Input
id="timeout"
type="number"
value={formData.timeout || ''}
onChange={(e) => !viewMode && handleInputChange('timeout', e.target.value)}
disabled={viewMode}
placeholder="30000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="retryCount"></Label>
<Input
id="retryCount"
type="number"
value={formData.retryCount || ''}
onChange={(e) => !viewMode && handleInputChange('retryCount', e.target.value)}
disabled={viewMode}
placeholder="3"
/>
</div>
<div className="space-y-2">
<Label htmlFor="team"></Label>
<Input
id="team"
value={formData.team || ''}
onChange={(e) => !viewMode && handleInputChange('team', e.target.value)}
disabled={viewMode}
placeholder="AI算法团队"
/>
</div>
</div>
</Card>
{/* 查询参数 */}
<Card className="p-4 bg-muted/30">
<h4 className="font-medium text-foreground mb-4"></h4>
<div className="space-y-3">
{Object.entries(model.parameters).map(([key, value]) => (
<div key={key} className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium text-foreground">{key}</div>
<div className="text-sm text-muted-foreground">
{typeof value === 'object' ? JSON.stringify(value) : value.toString()}
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(
typeof value === 'object' ? JSON.stringify(value) : value.toString()
)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
))}
</div>
</Card>
{/* 操作日志 */}
<Card className="p-4 bg-muted/30">
<h4 className="font-medium text-foreground mb-4"></h4>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium text-foreground"></div>
<div className="text-sm text-muted-foreground">2024-03-20 14:30:00 by admin</div>
</div>
<Badge className="bg-green-100 text-green-800"></Badge>
</div>
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium text-foreground"></div>
<div className="text-sm text-muted-foreground">2024-03-19 10:15:00 by admin</div>
</div>
<Badge className="bg-blue-100 text-blue-800"></Badge>
</div>
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium text-foreground"></div>
<div className="text-sm text-muted-foreground">2024-03-18 16:45:00 by admin</div>
</div>
<Badge className="bg-purple-100 text-purple-800"></Badge>
</div>
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium text-foreground"></div>
<div className="text-sm text-muted-foreground">2024-03-15 08:00:00 by admin</div>
</div>
<Badge className="bg-orange-100 text-orange-800"></Badge>
</div>
</div>
</Card>
{/* 运行统计信息 */}
<Card className="p-4 bg-muted/30">
<h4 className="font-medium text-foreground mb-4"></h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
{model.metrics.total_calls.toLocaleString()}
</div>
<div className="text-sm text-muted-foreground"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
{model.metrics.accuracy.toFixed(1)}%
</div>
<div className="text-sm text-muted-foreground"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-600 dark:text-orange-400">
{model.metrics.qps.toFixed(1)}
</div>
<div className="text-sm text-muted-foreground">QPS</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">
{model.metrics.success_rate}%
</div>
<div className="text-sm text-muted-foreground"></div>
</div>
</div>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
{viewMode ? '关闭' : '取消'}
</Button>
{!viewMode && (
<Button onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? '保存中...' : (
<>
<Save className="h-4 w-4 mr-2" />
</>
)}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,382 @@
/**
* filekorolheader: 模型服务列表组件 - 模型服务展示与管理
* 功能:模型列表展示、状态切换、多操作按钮、分页功能
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { ModelService } from '../types';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import {
Eye,
Edit,
Settings,
Users,
Zap,
RotateCcw,
Power,
Trash2
} from 'lucide-react';
import { ExternalLink } from 'lucide-react';
interface ModelServiceListProps {
models: ModelService[];
onEdit: (model: ModelService) => void;
onView: (model: ModelService) => void;
onDelete: (model: ModelService) => void;
onToggle: (modelId: string) => void;
onPermission: (model: ModelService) => void;
onPerformance: (model: ModelService) => void;
onRedeploy: (model: ModelService) => void;
loading: boolean;
}
export function ModelServiceList({
models,
onEdit,
onView,
onDelete,
onToggle,
onPermission,
onPerformance,
onRedeploy,
loading
}: ModelServiceListProps) {
const getModelTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'pest_identification': '病虫害识别',
'growth_status': '生长状态',
'yield_prediction': '产量预测'
};
return labels[type] || type;
};
const getStatusBadge = (status: string) => {
const statusConfig = {
active: {
label: '运行中',
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
},
inactive: {
label: '已停止',
className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
},
testing: {
label: '测试中',
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
},
error: {
label: '错误',
className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
}
};
const config = statusConfig[status as keyof typeof statusConfig];
return (
<Badge className={config.className}>
{config.label}
</Badge>
);
};
const getVisibilityBadge = (visibility: string) => {
const visibilityConfig = {
public: {
label: '公开',
className: 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
},
private: {
label: '私有',
className: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
},
team: {
label: '团队',
className: 'bg-blue-50 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
}
};
const config = visibilityConfig[visibility as keyof typeof visibilityConfig];
return (
<Badge variant="outline" className={config.className}>
{config.label}
</Badge>
);
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 60) {
return `${diffMins}分钟前`;
} else if (diffHours < 24) {
return `${diffHours}小时前`;
} else if (diffDays < 7) {
return `${diffDays}天前`;
} else {
return date.toLocaleDateString('zh-CN');
}
};
if (loading) {
return (
<Card className="p-8">
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<span className="ml-3 text-muted-foreground">...</span>
</div>
</Card>
);
}
if (models.length === 0) {
return (
<Card className="p-8">
<div className="text-center py-12">
<div className="text-6xl mb-4">🤖</div>
<h3 className="text-lg font-medium text-muted-foreground mb-2">
</h3>
<p className="text-sm text-muted-foreground">
"新增模型"AI模型服务
</p>
</div>
</Card>
);
}
return (
<Card className="bg-card">
<div className="p-6 border-b border-border">
<h3 className="text-lg font-semibold text-foreground"></h3>
<p className="text-sm text-muted-foreground mt-1">
AI模型服务的运行状态
</p>
</div>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>/</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>访</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{models.map((model) => (
<TableRow key={model.id} className="hover:bg-muted/50">
<TableCell>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
{model.name.charAt(0).toUpperCase()}
</div>
<div>
<div className="font-medium text-foreground">
{model.name}
</div>
{model.tags.length > 0 && (
<div className="flex gap-1 mt-1">
{model.tags.slice(0, 2).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{model.tags.length > 2 && (
<Badge variant="outline" className="text-xs">
+{model.tags.length - 2}
</Badge>
)}
</div>
)}
</div>
</div>
</TableCell>
<TableCell>
<div className="text-sm">
<div className="font-medium">{getModelTypeLabel(model.type)}</div>
<div className="text-muted-foreground">TensorFlow</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline">
{model.version}
</Badge>
</TableCell>
<TableCell>
<div className="text-sm space-y-1">
<div className="flex items-center">
<span className="text-muted-foreground w-12">:</span>
<span className={`font-medium ${
model.metrics.accuracy >= 95 ? 'text-green-600 dark:text-green-400' :
model.metrics.accuracy >= 85 ? 'text-yellow-600 dark:text-yellow-400' :
'text-red-600 dark:text-red-400'
}`}>
{model.metrics.accuracy.toFixed(1)}%
</span>
</div>
<div className="flex items-center">
<span className="text-muted-foreground w-12">:</span>
<span className="text-muted-foreground">{model.metrics.avg_response_time}ms</span>
</div>
</div>
</TableCell>
<TableCell>
<div className="text-sm space-y-1">
<div className="flex items-center">
<span className="text-muted-foreground w-12">QPS:</span>
<span className="font-medium">{model.metrics.qps.toFixed(1)}</span>
</div>
<div className="flex items-center">
<span className="text-muted-foreground w-12">:</span>
<span className="text-muted-foreground">{model.metrics.total_calls.toLocaleString()}</span>
</div>
</div>
</TableCell>
<TableCell>
{getVisibilityBadge(model.visibility)}
</TableCell>
<TableCell>
<div className="max-w-32">
<div className="text-xs font-mono truncate">
{model.apiEndpoint}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => window.open(model.apiEndpoint, '_blank')}
title="访问API"
className="h-6 px-2 mt-1"
>
<ExternalLink className="h-3 w-3" />
</Button>
</div>
</TableCell>
<TableCell>
{getStatusBadge(model.status)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{/* 停止/启动 */}
<Button
variant="ghost"
size="sm"
onClick={() => onToggle(model.id)}
title={model.status === 'active' ? '停止服务' : '启动服务'}
className="h-8 w-8 p-0"
>
<Power className={`h-4 w-4 ${
model.status === 'active' ? 'text-orange-500' : 'text-green-500'
}`} />
</Button>
{/* 查看详情 */}
<Button
variant="ghost"
size="sm"
onClick={() => onView(model)}
title="查看详情"
className="h-8 w-8 p-0"
>
<Eye className="h-4 w-4 text-blue-500" />
</Button>
{/* 编辑 */}
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(model)}
title="编辑模型"
className="h-8 w-8 p-0"
>
<Edit className="h-4 w-4 text-yellow-500" />
</Button>
{/* 权限管理 */}
<Button
variant="ghost"
size="sm"
onClick={() => onPermission(model)}
title="权限管理"
className="h-8 w-8 p-0"
>
<Users className="h-4 w-4 text-purple-500" />
</Button>
{/* 性能调优 */}
<Button
variant="ghost"
size="sm"
onClick={() => onPerformance(model)}
title="性能调优"
className="h-8 w-8 p-0"
>
<Zap className="h-4 w-4 text-cyan-500" />
</Button>
{/* 重新部署 */}
<Button
variant="ghost"
size="sm"
onClick={() => onRedeploy(model)}
title="重新部署"
className="h-8 w-8 p-0"
>
<RotateCcw className="h-4 w-4 text-indigo-500" />
</Button>
{/* 删除 */}
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(model)}
title="删除模型"
className="h-8 w-8 p-0"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="p-4 border-t border-border bg-muted/30">
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>
{models.length} |
: {models.filter(m => m.status === 'active').length} |
: {models.filter(m => m.status === 'testing').length}
</span>
<span>
: {models.reduce((sum, m) => sum + m.metrics.total_calls, 0).toLocaleString()}
</span>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,465 @@
/**
* filekorolheader: 性能调优对话框组件 - 模型性能参数优化
* 功能:当前性能指标、负载均衡策略、缓存优化配置
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import {
Zap,
Activity,
Server,
Database,
Gauge,
TrendingUp,
Settings
} from 'lucide-react';
import { toast } from 'sonner';
interface PerformanceDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: any;
}
export function PerformanceDialog({ open, onOpenChange, model }: PerformanceDialogProps) {
const [performanceSettings, setPerformanceSettings] = useState({
// 基础性能配置
concurrency: model?.concurrency || 100,
timeout: model?.timeout || 30000,
retryCount: model?.retryCount || 3,
// 负载均衡策略
loadBalancing: {
strategy: 'round_robin',
healthCheck: true,
healthCheckInterval: 30,
failoverEnabled: true,
maxFailures: 3
},
// 缓存配置
caching: {
enabled: true,
ttl: 300,
maxSize: 1000,
strategy: 'lru'
},
// 资源限制
resources: {
maxMemory: '2GB',
maxCpu: '50%',
maxGpu: '1'
}
});
const handleBasicSettingChange = (field: string, value: any) => {
setPerformanceSettings(prev => ({
...prev,
[field]: value
}));
};
const handleLoadBalancingChange = (field: string, value: any) => {
setPerformanceSettings(prev => ({
...prev,
loadBalancing: {
...prev.loadBalancing,
[field]: value
}
}));
};
const handleCachingChange = (field: string, value: any) => {
setPerformanceSettings(prev => ({
...prev,
caching: {
...prev.caching,
[field]: value
}
}));
};
const handleResourcesChange = (field: string, value: any) => {
setPerformanceSettings(prev => ({
...prev,
resources: {
...prev.resources,
[field]: value
}
}));
};
const savePerformanceSettings = () => {
toast.success('性能配置已保存');
onOpenChange(false);
};
// 模拟当前性能指标
const currentMetrics = {
cpuUsage: 65,
memoryUsage: 78,
gpuUsage: 45,
avgResponseTime: model?.metrics?.avg_response_time || 245,
qps: model?.metrics?.qps || 12.5,
cacheHitRate: 82,
errorRate: 1.5
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<strong>{model?.name}</strong>
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* 当前性能指标 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Gauge className="w-5 h-5 text-blue-600" />
<Label className="text-base font-medium"></Label>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between mb-2">
<Activity className="w-4 h-4 text-blue-600" />
<span className="text-sm font-medium">CPU使用率</span>
</div>
<div className="text-2xl font-bold">{currentMetrics.cpuUsage}%</div>
<Progress value={currentMetrics.cpuUsage} className="mt-2" />
</Card>
<Card className="p-4">
<div className="flex items-center justify-between mb-2">
<Database className="w-4 h-4 text-green-600" />
<span className="text-sm font-medium">使</span>
</div>
<div className="text-2xl font-bold">{currentMetrics.memoryUsage}%</div>
<Progress value={currentMetrics.memoryUsage} className="mt-2" />
</Card>
<Card className="p-4">
<div className="flex items-center justify-between mb-2">
<Zap className="w-4 h-4 text-purple-600" />
<span className="text-sm font-medium"></span>
</div>
<div className="text-2xl font-bold">{currentMetrics.avgResponseTime}ms</div>
<Badge
className={
currentMetrics.avgResponseTime < 200
? 'bg-green-100 text-green-800'
: currentMetrics.avgResponseTime < 500
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}
>
{currentMetrics.avgResponseTime < 200 ? '优秀' : currentMetrics.avgResponseTime < 500 ? '良好' : '需优化'}
</Badge>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between mb-2">
<TrendingUp className="w-4 h-4 text-orange-600" />
<span className="text-sm font-medium">QPS</span>
</div>
<div className="text-2xl font-bold">{currentMetrics.qps}</div>
<Badge className="bg-blue-100 text-blue-800">
</Badge>
</Card>
</div>
</div>
{/* 基础性能配置 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Settings className="w-5 h-5 text-green-600" />
<Label className="text-base font-medium"></Label>
</div>
<div className="grid grid-cols-3 gap-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={performanceSettings.concurrency}
onChange={(e) => handleBasicSettingChange('concurrency', parseInt(e.target.value) || 0)}
/>
</div>
<div className="space-y-2">
<Label> (ms)</Label>
<Input
type="number"
value={performanceSettings.timeout}
onChange={(e) => handleBasicSettingChange('timeout', parseInt(e.target.value) || 0)}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={performanceSettings.retryCount}
onChange={(e) => handleBasicSettingChange('retryCount', parseInt(e.target.value) || 0)}
/>
</div>
</div>
</div>
{/* 负载均衡策略 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Server className="w-5 h-5 text-orange-600" />
<Label className="text-base font-medium"></Label>
</div>
<Card className="p-4">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select
value={performanceSettings.loadBalancing.strategy}
onValueChange={(value) => handleLoadBalancingChange('strategy', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="round_robin"> (Round Robin)</SelectItem>
<SelectItem value="least_connections"> (Least Connections)</SelectItem>
<SelectItem value="weighted_round_robin"> (Weighted Round Robin)</SelectItem>
<SelectItem value="random"> (Random)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> ()</Label>
<Input
type="number"
value={performanceSettings.loadBalancing.healthCheckInterval}
onChange={(e) => handleLoadBalancingChange('healthCheckInterval', parseInt(e.target.value) || 0)}
disabled={!performanceSettings.loadBalancing.healthCheck}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground">
</div>
</div>
<Switch
checked={performanceSettings.loadBalancing.healthCheck}
onCheckedChange={(checked) => handleLoadBalancingChange('healthCheck', checked)}
/>
</div>
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground">
</div>
</div>
<Switch
checked={performanceSettings.loadBalancing.failoverEnabled}
onCheckedChange={(checked) => handleLoadBalancingChange('failoverEnabled', checked)}
/>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={performanceSettings.loadBalancing.maxFailures}
onChange={(e) => handleLoadBalancingChange('maxFailures', parseInt(e.target.value) || 0)}
disabled={!performanceSettings.loadBalancing.failoverEnabled}
/>
</div>
</div>
</Card>
</div>
{/* 缓存优化 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Database className="w-5 h-5 text-purple-600" />
<Label className="text-base font-medium"></Label>
</div>
<Card className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground">
</div>
</div>
<Switch
checked={performanceSettings.caching.enabled}
onCheckedChange={(checked) => handleCachingChange('enabled', checked)}
/>
</div>
{performanceSettings.caching.enabled && (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label> ()</Label>
<Input
type="number"
value={performanceSettings.caching.ttl}
onChange={(e) => handleCachingChange('ttl', parseInt(e.target.value) || 0)}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={performanceSettings.caching.maxSize}
onChange={(e) => handleCachingChange('maxSize', parseInt(e.target.value) || 0)}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select
value={performanceSettings.caching.strategy}
onValueChange={(value) => handleCachingChange('strategy', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="lru">使 (LRU)</SelectItem>
<SelectItem value="lfu">使 (LFU)</SelectItem>
<SelectItem value="fifo"> (FIFO)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
<div className="grid grid-cols-3 gap-4">
<Card className="p-3">
<div className="text-sm text-muted-foreground"></div>
<div className="text-lg font-bold text-green-600">{currentMetrics.cacheHitRate}%</div>
</Card>
<Card className="p-3">
<div className="text-sm text-muted-foreground"></div>
<div className="text-lg font-bold text-red-600">{currentMetrics.errorRate}%</div>
</Card>
<Card className="p-3">
<div className="text-sm text-muted-foreground">GPU使用率</div>
<div className="text-lg font-bold text-purple-600">{currentMetrics.gpuUsage}%</div>
</Card>
</div>
</div>
</Card>
</div>
{/* 资源限制 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Activity className="w-5 h-5 text-red-600" />
<Label className="text-base font-medium"></Label>
</div>
<div className="grid grid-cols-3 gap-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label></Label>
<Select
value={performanceSettings.resources.maxMemory}
onValueChange={(value) => handleResourcesChange('maxMemory', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1GB">1GB</SelectItem>
<SelectItem value="2GB">2GB</SelectItem>
<SelectItem value="4GB">4GB</SelectItem>
<SelectItem value="8GB">8GB</SelectItem>
<SelectItem value="16GB">16GB</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>CPU</Label>
<Select
value={performanceSettings.resources.maxCpu}
onValueChange={(value) => handleResourcesChange('maxCpu', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="25%">25%</SelectItem>
<SelectItem value="50%">50%</SelectItem>
<SelectItem value="75%">75%</SelectItem>
<SelectItem value="100%">100%</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>GPU</Label>
<Select
value={performanceSettings.resources.maxGpu}
onValueChange={(value) => handleResourcesChange('maxGpu', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">使</SelectItem>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="4">4</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={savePerformanceSettings}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,294 @@
/**
* filekorolheader: 权限管理对话框组件 - 模型访问权限设置
* 功能访问级别、API限流配置、IP白名单管理
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Plus, X, Shield, Clock, Globe } from 'lucide-react';
import { toast } from 'sonner';
interface PermissionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: any;
}
export function PermissionDialog({ open, onOpenChange, model }: PermissionDialogProps) {
const [accessLevel, setAccessLevel] = useState(model?.visibility || 'private');
const [ipWhitelist, setIpWhitelist] = useState<string[]>(['192.168.1.100', '10.0.0.50']);
const [newIp, setNewIp] = useState('');
const [apiRateLimit, setApiRateLimit] = useState({
enabled: true,
requestsPerMinute: 100,
requestsPerHour: 1000,
requestsPerDay: 10000
});
const [authRequired, setAuthRequired] = useState(true);
const [apiKeyRequired, setApiKeyRequired] = useState(true);
const addIpToWhitelist = () => {
if (newIp && !ipWhitelist.includes(newIp)) {
setIpWhitelist([...ipWhitelist, newIp]);
setNewIp('');
}
};
const removeIpFromWhitelist = (ip: string) => {
setIpWhitelist(ipWhitelist.filter(ipAddr => ipAddr !== ip));
};
const savePermissions = () => {
toast.success('权限设置已保存');
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<strong>{model?.name}</strong> 访
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* 访问级别 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-blue-600" />
<Label className="text-base font-medium">访</Label>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50">
<div className="flex items-center space-x-3">
<input
type="radio"
name="access"
checked={accessLevel === 'public'}
onChange={() => setAccessLevel('public')}
className="w-4 h-4"
/>
<div>
<div className="font-medium">访</div>
<div className="text-sm text-muted-foreground">
访API
</div>
</div>
</div>
<Badge className="bg-green-100 text-green-800"></Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50">
<div className="flex items-center space-x-3">
<input
type="radio"
name="access"
checked={accessLevel === 'team'}
onChange={() => setAccessLevel('team')}
className="w-4 h-4"
/>
<div>
<div className="font-medium">访</div>
<div className="text-sm text-muted-foreground">
访
</div>
</div>
</div>
<Badge className="bg-blue-100 text-blue-800"></Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50">
<div className="flex items-center space-x-3">
<input
type="radio"
name="access"
checked={accessLevel === 'private'}
onChange={() => setAccessLevel('private')}
className="w-4 h-4"
/>
<div>
<div className="font-medium">访</div>
<div className="text-sm text-muted-foreground">
访
</div>
</div>
</div>
<Badge className="bg-red-100 text-red-800"></Badge>
</div>
</div>
</div>
{/* API限流配置 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-orange-600" />
<Label className="text-base font-medium">API限流配置</Label>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<div className="font-medium">API限流</div>
<div className="text-sm text-muted-foreground">
API调用频率
</div>
</div>
<Switch
checked={apiRateLimit.enabled}
onCheckedChange={(checked) => setApiRateLimit(prev => ({ ...prev, enabled: checked }))}
/>
</div>
{apiRateLimit.enabled && (
<div className="grid grid-cols-3 gap-4 p-4 border rounded-lg bg-muted/30">
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={apiRateLimit.requestsPerMinute}
onChange={(e) => setApiRateLimit(prev => ({
...prev,
requestsPerMinute: parseInt(e.target.value) || 0
}))}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={apiRateLimit.requestsPerHour}
onChange={(e) => setApiRateLimit(prev => ({
...prev,
requestsPerHour: parseInt(e.target.value) || 0
}))}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={apiRateLimit.requestsPerDay}
onChange={(e) => setApiRateLimit(prev => ({
...prev,
requestsPerDay: parseInt(e.target.value) || 0
}))}
/>
</div>
</div>
)}
</div>
{/* IP白名单 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-green-600" />
<Label className="text-base font-medium">IP白名单</Label>
</div>
<div className="space-y-3">
<div className="flex space-x-2">
<Input
placeholder="输入IP地址添加到白名单"
value={newIp}
onChange={(e) => setNewIp(e.target.value)}
className="flex-1"
/>
<Button onClick={addIpToWhitelist} size="sm">
<Plus className="w-4 h-4" />
</Button>
</div>
<div className="space-y-2">
{ipWhitelist.map((ip, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-muted rounded">
<div className="flex items-center space-x-2">
<Globe className="w-4 h-4 text-muted-foreground" />
<code className="text-sm font-mono">{ip}</code>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeIpFromWhitelist(ip)}
>
<X className="w-4 h-4" />
</Button>
</div>
))}
{ipWhitelist.length === 0 && (
<div className="text-center text-muted-foreground py-4">
IP白名单配置
</div>
)}
</div>
</div>
</div>
{/* 认证设置 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-purple-600" />
<Label className="text-base font-medium"></Label>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground">
访API前需要用户登录验证
</div>
</div>
<Switch
checked={authRequired}
onCheckedChange={setAuthRequired}
/>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<div className="font-medium">API密钥</div>
<div className="text-sm text-muted-foreground">
API调用需要提供有效的API密钥
</div>
</div>
<Switch
checked={apiKeyRequired}
onCheckedChange={setApiKeyRequired}
/>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={savePermissions}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,223 @@
/**
* filekorolheader: 重新部署对话框组件 - 模型服务重新部署
* 功能:部署确认、版本选择、部署进度监控
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { RotateCcw, AlertTriangle, CheckCircle, Clock } from 'lucide-react';
import { toast } from 'sonner';
interface RedeployDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
model: any;
dispatch: React.Dispatch<any>;
}
export function RedeployDialog({ open, onOpenChange, model, dispatch }: RedeployDialogProps) {
const [deployVersion, setDeployVersion] = useState(model?.version || 'latest');
const [isDeploying, setIsDeploying] = useState(false);
const [deployProgress, setDeployProgress] = useState(0);
const [deployStatus, setDeployStatus] = useState<'idle' | 'deploying' | 'success' | 'error'>('idle');
const handleRedeploy = async () => {
setIsDeploying(true);
setDeployStatus('deploying');
setDeployProgress(0);
try {
// 模拟部署过程
const steps = [
{ progress: 20, message: '停止当前服务...' },
{ progress: 40, message: '备份当前版本...' },
{ progress: 60, message: '加载新版本模型...' },
{ progress: 80, message: '运行健康检查...' },
{ progress: 95, message: '重启服务...' },
{ progress: 100, message: '部署完成' }
];
for (const step of steps) {
await new Promise(resolve => setTimeout(resolve, 1000));
setDeployProgress(step.progress);
}
setDeployStatus('success');
toast.success('模型重新部署成功!');
// 更新模型状态
setTimeout(() => {
onOpenChange(false);
setDeployStatus('idle');
setDeployProgress(0);
}, 2000);
} catch (error) {
setDeployStatus('error');
toast.error('部署失败,请重试');
console.error('Deploy error:', error);
} finally {
setIsDeploying(false);
}
};
const availableVersions = [
{ version: 'v2.1.1', status: 'stable', date: '2024-03-21' },
{ version: 'v2.1.0', status: 'current', date: '2024-03-20' },
{ version: 'v2.0.9', status: 'stable', date: '2024-03-15' },
{ version: 'v2.1.2-beta', status: 'beta', date: '2024-03-22' }
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<strong>{model?.name}</strong>
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{deployStatus === 'idle' && (
<>
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
</AlertDescription>
</Alert>
<div className="space-y-2">
<Label></Label>
<Select value={deployVersion} onValueChange={setDeployVersion}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableVersions.map((version) => (
<SelectItem key={version.version} value={version.version}>
<div className="flex items-center justify-between w-full">
<span>{version.version}</span>
<div className="flex items-center gap-2">
<Badge
variant={
version.status === 'current'
? 'default'
: version.status === 'stable'
? 'secondary'
: 'outline'
}
className="text-xs"
>
{version.status === 'current' ? '当前' :
version.status === 'stable' ? '稳定' : '测试'}
</Badge>
<span className="text-xs text-muted-foreground">
{version.date}
</span>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="text-sm text-muted-foreground">
<div className="font-medium mb-2"></div>
<ul className="space-y-1 ml-4">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</>
)}
{deployStatus === 'deploying' && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<RotateCcw className="w-4 h-4 animate-spin text-blue-600" />
<span className="font-medium">...</span>
</div>
<Progress value={deployProgress} className="w-full" />
<div className="text-sm text-muted-foreground text-center">
{deployProgress < 20 && '准备部署环境...'}
{deployProgress >= 20 && deployProgress < 40 && '停止当前服务...'}
{deployProgress >= 40 && deployProgress < 60 && '备份当前版本...'}
{deployProgress >= 60 && deployProgress < 80 && '加载新版本模型...'}
{deployProgress >= 80 && deployProgress < 95 && '运行健康检查...'}
{deployProgress >= 95 && '重启服务...'}
</div>
</div>
)}
{deployStatus === 'success' && (
<div className="space-y-4">
<div className="flex items-center gap-2 text-green-600">
<CheckCircle className="w-5 h-5" />
<span className="font-medium"></span>
</div>
<div className="text-sm text-muted-foreground">
{deployVersion}...
</div>
</div>
)}
{deployStatus === 'error' && (
<div className="space-y-4">
<div className="flex items-center gap-2 text-red-600">
<AlertTriangle className="w-5 h-5" />
<span className="font-medium"></span>
</div>
<div className="text-sm text-muted-foreground">
</div>
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isDeploying}
>
{deployStatus === 'success' ? '完成' : '取消'}
</Button>
{deployStatus === 'idle' && (
<Button onClick={handleRedeploy} disabled={isDeploying}>
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,390 @@
/**
* filekorolheader: 模型集成管理状态管理 - 模型服务与参数集中管理
* 功能:模型状态管理、弹窗控制、数据持久化、筛选功能
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式
*/
import { ModelService, FilterOptions } from '../types';
export interface ModelIntegrationState {
// 模型服务数据
models: ModelService[];
// 筛选选项
filters: FilterOptions;
// 统计数据
totalModels: number;
runningModels: number;
avgAccuracy: number;
qps: number;
// 弹窗状态
showAddModelDialog: boolean;
showConfigDialog: boolean;
showViewDialog: boolean;
showDeleteDialog: boolean;
showPermissionDialog: boolean;
showPerformanceDialog: boolean;
showRedeployDialog: boolean;
// 选中的模型
selectedModel: ModelService | null;
// 加载状态
loading: boolean;
}
export type ModelIntegrationAction =
| { type: 'LOAD_DATA' }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_MODELS'; payload: ModelService[] }
| { type: 'UPDATE_FILTERS'; payload: Partial<FilterOptions> }
| { type: 'ADD_MODEL'; payload: ModelService }
| { type: 'UPDATE_MODEL'; payload: ModelService }
| { type: 'DELETE_MODEL'; payload: string }
| { type: 'TOGGLE_MODEL_STATUS'; payload: string }
| { type: 'SET_SELECTED_MODEL'; payload: ModelService | null }
| { type: 'SHOW_ADD_MODEL_DIALOG' }
| { type: 'SHOW_CONFIG_DIALOG'; payload: ModelService }
| { type: 'SHOW_VIEW_DIALOG'; payload: ModelService }
| { type: 'SHOW_DELETE_DIALOG'; payload: ModelService }
| { type: 'SHOW_PERMISSION_DIALOG'; payload: ModelService }
| { type: 'SHOW_PERFORMANCE_DIALOG'; payload: ModelService }
| { type: 'SHOW_REDEPLOY_DIALOG'; payload: ModelService }
| { type: 'SET_ADD_MODEL_DIALOG'; payload: boolean }
| { type: 'SET_CONFIG_DIALOG'; payload: boolean }
| { type: 'SET_VIEW_DIALOG'; payload: boolean }
| { type: 'SET_DELETE_DIALOG'; payload: boolean }
| { type: 'SET_PERMISSION_DIALOG'; payload: boolean }
| { type: 'SET_PERFORMANCE_DIALOG'; payload: boolean }
| { type: 'SET_REDEPLOY_DIALOG'; payload: boolean };
const generateMockModels = (): ModelService[] => [
{
id: '1',
name: '病虫害识别模型',
type: 'pest_identification',
version: 'v2.1.0',
status: 'active',
description: '基于深度学习的农作物病虫害图像识别模型支持52种常见病虫害识别',
apiEndpoint: 'https://api.smart-crop.com/v2/models/pest-detection',
parameters: {
input_size: [224, 224],
confidence_threshold: 0.85,
max_detections: 10
},
metrics: {
total_calls: 15420,
avg_response_time: 245,
success_rate: 98.5,
accuracy: 95.2,
last_called: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
qps: 12.5
},
tags: ['图像识别', '病虫害', '深度学习'],
visibility: 'public',
team: 'AI算法团队',
concurrency: 100,
timeout: 30000,
retryCount: 3,
createdAt: '2024-03-15T08:00:00Z',
updatedAt: '2024-03-20T14:30:00Z'
},
{
id: '2',
name: '产量预测模型',
type: 'yield_prediction',
version: 'v1.3.2',
status: 'active',
description: '基于历史数据和环境因素的农作物产量预测模型',
apiEndpoint: 'https://api.smart-crop.com/v2/models/yield-prediction',
parameters: {
weather_features: true,
soil_features: true,
historical_days: 30,
prediction_horizon: 90
},
metrics: {
total_calls: 8960,
avg_response_time: 890,
success_rate: 99.1,
accuracy: 88.7,
last_called: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
qps: 8.2
},
tags: ['预测分析', '产量', '机器学习'],
visibility: 'team',
team: '数据分析团队',
concurrency: 50,
timeout: 60000,
retryCount: 2,
createdAt: '2024-02-20T10:00:00Z',
updatedAt: '2024-03-18T16:45:00Z'
},
{
id: '3',
name: '生长状态识别模型',
type: 'growth_status',
version: 'v1.2.0',
status: 'active',
description: '基于作物图像的生长阶段自动识别模型',
apiEndpoint: 'https://api.smart-crop.com/v2/models/growth-stage',
parameters: {
input_size: [256, 256],
confidence_threshold: 0.75,
growth_stages: ['seedling', 'vegetative', 'flowering', 'fruiting', 'mature']
},
metrics: {
total_calls: 2150,
avg_response_time: 445,
success_rate: 94.2,
accuracy: 89.3,
last_called: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
qps: 4.2
},
tags: ['图像识别', '生长阶段', 'AI分析'],
visibility: 'public',
team: 'AI算法团队',
concurrency: 20,
timeout: 25000,
retryCount: 1,
createdAt: '2024-03-10T15:00:00Z',
updatedAt: '2024-03-22T10:30:00Z'
}
];
const calculateStatistics = (models: ModelService[]) => {
const totalModels = models.length;
const runningModels = models.filter(m => m.status === 'active').length;
const avgAccuracy = models.length > 0
? (models.reduce((sum, model) => sum + model.metrics.accuracy, 0) / models.length).toFixed(1)
: 0;
const qps = models.length > 0
? models.reduce((sum, model) => sum + model.metrics.qps, 0).toFixed(1)
: 0;
return {
totalModels,
runningModels,
avgAccuracy: parseFloat(avgAccuracy),
qps: parseFloat(qps)
};
};
export const initialState: ModelIntegrationState = {
models: [],
filters: {
searchTerm: '',
typeFilter: 'all',
statusFilter: 'all'
},
totalModels: 0,
runningModels: 0,
avgAccuracy: 0,
qps: 0,
showAddModelDialog: false,
showConfigDialog: false,
showViewDialog: false,
showDeleteDialog: false,
showPermissionDialog: false,
showPerformanceDialog: false,
showRedeployDialog: false,
selectedModel: null,
loading: false
};
export const modelIntegrationReducer = (
state: ModelIntegrationState,
action: ModelIntegrationAction
): ModelIntegrationState => {
switch (action.type) {
case 'LOAD_DATA':
const models = generateMockModels();
const statistics = calculateStatistics(models);
// 持久化到localStorage
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(models));
return {
...state,
models,
...statistics,
loading: false
};
case 'SET_LOADING':
return {
...state,
loading: action.payload
};
case 'SET_MODELS':
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(action.payload));
const updatedStatistics = calculateStatistics(action.payload);
return {
...state,
models: action.payload,
...updatedStatistics
};
case 'UPDATE_FILTERS':
const newFilters = { ...state.filters, ...action.payload };
return {
...state,
filters: newFilters
};
case 'ADD_MODEL':
const newModels = [...state.models, action.payload];
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(newModels));
const addStatistics = calculateStatistics(newModels);
return {
...state,
models: newModels,
...addStatistics
};
case 'UPDATE_MODEL':
const updatedModels = state.models.map(model =>
model.id === action.payload.id ? { ...action.payload, updatedAt: new Date().toISOString() } : model
);
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(updatedModels));
const updateStatistics = calculateStatistics(updatedModels);
return {
...state,
models: updatedModels,
...updateStatistics
};
case 'DELETE_MODEL':
const filteredModels = state.models.filter(model => model.id !== action.payload);
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(filteredModels));
const deleteStatistics = calculateStatistics(filteredModels);
return {
...state,
models: filteredModels,
...deleteStatistics
};
case 'TOGGLE_MODEL_STATUS':
const toggledModels = state.models.map(model =>
model.id === action.payload
? {
...model,
status: model.status === 'active' ? 'inactive' : 'active' as 'active' | 'inactive',
updatedAt: new Date().toISOString()
}
: model
);
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(toggledModels));
const toggleStatistics = calculateStatistics(toggledModels);
return {
...state,
models: toggledModels,
...toggleStatistics
};
case 'SET_SELECTED_MODEL':
return {
...state,
selectedModel: action.payload
};
case 'SHOW_ADD_MODEL_DIALOG':
return {
...state,
showAddModelDialog: true
};
case 'SHOW_CONFIG_DIALOG':
return {
...state,
showConfigDialog: true,
selectedModel: action.payload
};
case 'SHOW_VIEW_DIALOG':
return {
...state,
showViewDialog: true,
selectedModel: action.payload
};
case 'SHOW_DELETE_DIALOG':
return {
...state,
showDeleteDialog: true,
selectedModel: action.payload
};
case 'SHOW_PERMISSION_DIALOG':
return {
...state,
showPermissionDialog: true,
selectedModel: action.payload
};
case 'SHOW_PERFORMANCE_DIALOG':
return {
...state,
showPerformanceDialog: true,
selectedModel: action.payload
};
case 'SHOW_REDEPLOY_DIALOG':
return {
...state,
showRedeployDialog: true,
selectedModel: action.payload
};
case 'SET_ADD_MODEL_DIALOG':
return {
...state,
showAddModelDialog: action.payload
};
case 'SET_CONFIG_DIALOG':
return {
...state,
showConfigDialog: action.payload
};
case 'SET_VIEW_DIALOG':
return {
...state,
showViewDialog: action.payload
};
case 'SET_DELETE_DIALOG':
return {
...state,
showDeleteDialog: action.payload
};
case 'SET_PERMISSION_DIALOG':
return {
...state,
showPermissionDialog: action.payload
};
case 'SET_PERFORMANCE_DIALOG':
return {
...state,
showPerformanceDialog: action.payload
};
case 'SET_REDEPLOY_DIALOG':
return {
...state,
showRedeployDialog: action.payload
};
default:
return state;
}
};

View File

@@ -0,0 +1,356 @@
/**
* filekorolheader: 模型集成管理页面 - AI模型服务与参数管理平台
* 功能:模型服务列表管理、统计卡片展示、搜索筛选功能、模型操作管理
* 路径:/ai-crop-model/model-integration/management
* 规范遵循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 { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import {
Plus,
Brain,
PlayCircle,
Target,
Zap,
Search
} from 'lucide-react';
import { ModelServiceList } from './components/ModelServiceList';
import { ModelAnalytics } from './components/ModelAnalytics';
import { AddModelDialog } from './components/AddModelDialog';
import { ModelConfigDialog } from './components/ModelConfigDialog';
import { DeleteModelDialog } from './components/DeleteModelDialog';
import { PermissionDialog } from './components/PermissionDialog';
import { PerformanceDialog } from './components/PerformanceDialog';
import { RedeployDialog } from './components/RedeployDialog';
import { modelIntegrationReducer, initialState } from './components/modelIntegrationReducer';
import { ModelType, ModelStatus } from './types';
export default function ModelIntegrationPage() {
const [state, dispatch] = useReducer(modelIntegrationReducer, initialState);
useEffect(() => {
dispatch({ type: 'LOAD_DATA' });
}, []);
const handleAddModel = () => {
dispatch({ type: 'SHOW_ADD_MODEL_DIALOG' });
};
const handleEditModel = (model: any) => {
dispatch({ type: 'SHOW_CONFIG_DIALOG', payload: model });
};
const handleViewModel = (model: any) => {
dispatch({ type: 'SHOW_VIEW_DIALOG', payload: model });
};
const handleDeleteModel = (model: any) => {
dispatch({ type: 'SHOW_DELETE_DIALOG', payload: model });
};
const handleToggleModel = (modelId: string) => {
dispatch({ type: 'TOGGLE_MODEL_STATUS', payload: modelId });
};
const handlePermission = (model: any) => {
dispatch({ type: 'SHOW_PERMISSION_DIALOG', payload: model });
};
const handlePerformance = (model: any) => {
dispatch({ type: 'SHOW_PERFORMANCE_DIALOG', payload: model });
};
const handleRedeploy = (model: any) => {
dispatch({ type: 'SHOW_REDEPLOY_DIALOG', payload: model });
};
const handleSearchChange = (value: string) => {
dispatch({ type: 'UPDATE_FILTERS', payload: { searchTerm: value } });
};
const handleTypeFilterChange = (value: string) => {
dispatch({ type: 'UPDATE_FILTERS', payload: { typeFilter: value as ModelType | 'all' } });
};
const handleStatusFilterChange = (value: string) => {
dispatch({ type: 'UPDATE_FILTERS', payload: { statusFilter: value as ModelStatus | 'all' } });
};
// 过滤模型
const filteredModels = state.models.filter(model => {
const matchesSearch = model.name.toLowerCase().includes(state.filters.searchTerm.toLowerCase()) ||
model.description.toLowerCase().includes(state.filters.searchTerm.toLowerCase());
const matchesType = state.filters.typeFilter === 'all' || model.type === state.filters.typeFilter;
const matchesStatus = state.filters.statusFilter === 'all' || model.status === state.filters.statusFilter;
return matchesSearch && matchesType && matchesStatus;
});
return (
<div className="space-y-6">
{/* 页面头部 */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-primary"></h2>
<p className="text-muted-foreground">AI模型服务</p>
</div>
<Button onClick={handleAddModel}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 4个统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-blue-600 dark:text-blue-400 font-medium">
</div>
<div className="text-2xl font-bold text-blue-700 dark:text-blue-300 mt-1">
{state.totalModels}
</div>
<div className="text-xs text-blue-500 dark:text-blue-400 mt-1">
</div>
</div>
<Brain className="w-8 h-8 text-blue-500 dark:text-blue-400" />
</div>
</Card>
<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">
{state.runningModels}
</div>
<div className="text-xs text-green-500 dark:text-green-400 mt-1">
</div>
</div>
<PlayCircle className="w-8 h-8 text-green-500 dark:text-green-400" />
</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">
{state.avgAccuracy}%
</div>
<div className="text-xs text-purple-500 dark:text-purple-400 mt-1">
</div>
</div>
<Target className="w-8 h-8 text-purple-500 dark:text-purple-400" />
</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">
QPS
</div>
<div className="text-2xl font-bold text-orange-700 dark:text-orange-300 mt-1">
{state.qps}
</div>
<div className="text-xs text-orange-500 dark:text-orange-400 mt-1">
</div>
</div>
<Zap className="w-8 h-8 text-orange-500 dark:text-orange-400" />
</div>
</Card>
</div>
{/* 模型服务管理功能介绍 */}
<Card className="p-6 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<h3 className="text-lg font-semibold text-green-800 dark:text-green-200 mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
<div>
<div className="font-medium text-green-800 dark:text-green-200"></div>
<div className="text-sm text-green-600 dark:text-green-400"></div>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
<div>
<div className="font-medium text-green-800 dark:text-green-200"></div>
<div className="text-sm text-green-600 dark:text-green-400"></div>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
<div>
<div className="font-medium text-green-800 dark:text-green-200"></div>
<div className="text-sm text-green-600 dark:text-green-400"></div>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
<div>
<div className="font-medium text-green-800 dark:text-green-200"></div>
<div className="text-sm text-green-600 dark:text-green-400">//访</div>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
<div>
<div className="font-medium text-green-800 dark:text-green-200"></div>
<div className="text-sm text-green-600 dark:text-green-400"></div>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
<div>
<div className="font-medium text-green-800 dark:text-green-200"></div>
<div className="text-sm text-green-600 dark:text-green-400"></div>
</div>
</div>
</div>
</Card>
{/* 搜索和筛选区域 */}
<Card className="p-4">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<Label htmlFor="search"></Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
id="search"
placeholder="搜索模型名称或描述..."
value={state.filters.searchTerm}
onChange={(e) => handleSearchChange(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="w-full md:w-48">
<Label htmlFor="typeFilter"></Label>
<Select value={state.filters.typeFilter} onValueChange={handleTypeFilterChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="pest_identification"></SelectItem>
<SelectItem value="growth_status"></SelectItem>
<SelectItem value="yield_prediction"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-full md:w-48">
<Label htmlFor="statusFilter"></Label>
<Select value={state.filters.statusFilter} onValueChange={handleStatusFilterChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
<SelectItem value="testing"></SelectItem>
<SelectItem value="error"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Card>
{/* 模型分析图表 */}
<ModelAnalytics models={state.models} />
{/* 模型服务列表 */}
<ModelServiceList
models={filteredModels}
onEdit={handleEditModel}
onView={handleViewModel}
onDelete={handleDeleteModel}
onToggle={handleToggleModel}
onPermission={handlePermission}
onPerformance={handlePerformance}
onRedeploy={handleRedeploy}
loading={state.loading}
/>
{/* 新增模型对话框 */}
<AddModelDialog
open={state.showAddModelDialog}
onOpenChange={(open) => dispatch({ type: 'SET_ADD_MODEL_DIALOG', payload: open })}
dispatch={dispatch}
/>
{/* 权限管理对话框 */}
<PermissionDialog
open={state.showPermissionDialog}
onOpenChange={(open) => dispatch({ type: 'SET_PERMISSION_DIALOG', payload: open })}
model={state.selectedModel}
/>
{/* 性能调优对话框 */}
<PerformanceDialog
open={state.showPerformanceDialog}
onOpenChange={(open) => dispatch({ type: 'SET_PERFORMANCE_DIALOG', payload: open })}
model={state.selectedModel}
/>
{/* 重新部署对话框 */}
<RedeployDialog
open={state.showRedeployDialog}
onOpenChange={(open) => dispatch({ type: 'SET_REDEPLOY_DIALOG', payload: open })}
model={state.selectedModel}
dispatch={dispatch}
/>
{/* 模型配置对话框 */}
<ModelConfigDialog
open={state.showConfigDialog}
onOpenChange={(open) => dispatch({ type: 'SET_CONFIG_DIALOG', payload: open })}
model={state.selectedModel}
dispatch={dispatch}
/>
{/* 模型查看对话框 */}
<ModelConfigDialog
open={state.showViewDialog}
onOpenChange={(open) => dispatch({ type: 'SET_VIEW_DIALOG', payload: open })}
model={state.selectedModel}
viewMode={true}
dispatch={dispatch}
/>
{/* 删除确认对话框 */}
<DeleteModelDialog
open={state.showDeleteDialog}
onOpenChange={(open) => dispatch({ type: 'SET_DELETE_DIALOG', payload: open })}
model={state.selectedModel}
dispatch={dispatch}
/>
</div>
);
}

View File

@@ -0,0 +1,53 @@
/**
* filekorolheader: 模型集成管理类型定义 - 模型服务与参数类型规范
* 功能:模型服务接口定义、数据结构规范、枚举类型定义
* 路径:/ai-crop-model/model-integration/management
* 规范遵循crop-x/docs/开发项目规范.md使用TypeScript严格类型检查
*/
export type ModelType =
| 'image_classification'
| 'object_detection'
| 'regression'
| 'multiclass_classification'
| 'optimization'
| 'time_series'
| 'nlp'
| 'anomaly_detection';
export type ModelStatus = 'active' | 'inactive' | 'testing' | 'error';
export interface ModelMetrics {
total_calls: number;
avg_response_time: number;
success_rate: number;
accuracy: number; // 准确率
last_called: string;
qps: number; // 每秒查询数
}
export interface ModelService {
id: string;
name: string;
type: ModelType;
version: string;
status: ModelStatus;
description: string;
apiEndpoint: string;
parameters: Record<string, any>;
metrics: ModelMetrics;
tags: string[];
visibility: 'public' | 'private' | 'team';
team: string;
concurrency: number;
timeout: number;
retryCount: number;
createdAt: string;
updatedAt: string;
}
export interface FilterOptions {
searchTerm: string;
typeFilter: ModelType | 'all';
statusFilter: ModelStatus | 'all';
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ModelIntegrationPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/model-integration
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function AlertMonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/monitoring/alert
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function AuditMonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/monitoring/audit
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ModelMonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/monitoring/model
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function MonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/monitoring
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function AiCropModelPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold">AI作物模型精准决策系统</h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,187 @@
/**
* filekorolheader: 地块决策分布地图组件 - 地理位置决策可视化
* 功能:地块标记、状态可视化、悬浮详情、图例说明
* 路径:/ai-crop-model/support/dashboard/components/DecisionMap
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { MapPin, Map as MapIcon } from 'lucide-react';
import { FieldDecisionInfo } from './aiDecisionDashboardReducer';
interface DecisionMapProps {
fieldDecisions: FieldDecisionInfo[];
}
export function DecisionMap({ fieldDecisions }: DecisionMapProps) {
return (
<Card className="p-6 bg-card">
<div className="flex items-center justify-between mb-4">
<h3></h3>
<Badge variant="outline" className="font-light">
<MapIcon className="w-3 h-3 mr-1" />
{fieldDecisions.length}
</Badge>
</div>
{/* 模拟地图区域 */}
<div className="relative h-96 bg-gradient-to-br from-green-50 dark:from-green-950 to-blue-50 dark:to-blue-950 rounded-lg border-2 border-green-200 dark:border-green-800 overflow-hidden">
{/* 地图背景 */}
<div className="absolute inset-0 opacity-20">
<div className="w-full h-full" style={{
backgroundImage: 'repeating-linear-gradient(0deg, #10b981 0px, #10b981 1px, transparent 1px, transparent 20px), repeating-linear-gradient(90deg, #10b981 0px, #10b981 1px, transparent 1px, transparent 20px)',
}}></div>
</div>
{/* 地块标记点 */}
{fieldDecisions.map((field, index) => {
// 计算标记点位置(模拟分布)
const positions = [
{ top: '15%', left: '20%' },
{ top: '25%', left: '65%' },
{ top: '45%', left: '30%' },
{ top: '55%', left: '75%' },
{ top: '70%', left: '45%' },
{ top: '35%', left: '85%' },
{ top: '80%', left: '25%' },
];
const position = positions[index] || { top: '50%', left: '50%' };
return (
<div
key={field.fieldId}
className="absolute transform -translate-x-1/2 -translate-y-1/2 group cursor-pointer"
style={position}
>
{/* 标记点 */}
<div className={`w-10 h-10 rounded-full flex items-center justify-center shadow-lg transition-transform hover:scale-125 ${
field.urgentCount > 0 ? 'bg-red-500 dark:bg-red-600' :
field.generatedCount > 0 ? 'bg-blue-500 dark:bg-blue-600' :
field.executingCount > 0 ? 'bg-purple-500 dark:bg-purple-600' :
'bg-green-500 dark:bg-green-600'
}`}>
<MapPin className="w-5 h-5 text-white" />
</div>
{/* 决策数量标记 */}
{field.decisions.length > 0 && (
<div className="absolute -top-2 -right-2 w-6 h-6 bg-yellow-500 dark:bg-yellow-600 text-white rounded-full flex items-center justify-center text-xs font-bold shadow-lg">
{field.decisions.length}
</div>
)}
{/* 悬浮信息卡片 */}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-xl p-4 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10 border-2 border-green-200 dark:border-green-800">
<div className="mb-2">
<div className="flex items-center gap-2 mb-1">
<MapPin className="w-4 h-4 text-green-600 dark:text-green-400" />
<strong>{field.fieldName}</strong>
</div>
<div className="text-xs text-muted-foreground">
{field.cropType} · {field.area}
</div>
</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<strong>{field.decisions.length}</strong>
</div>
{field.urgentCount > 0 && (
<div className="flex justify-between text-red-600 dark:text-red-400">
<span>:</span>
<strong>{field.urgentCount}</strong>
</div>
)}
{field.generatedCount > 0 && (
<div className="flex justify-between text-blue-600 dark:text-blue-400">
<span>:</span>
<strong>{field.generatedCount}</strong>
</div>
)}
{field.executingCount > 0 && (
<div className="flex justify-between text-purple-600 dark:text-purple-400">
<span>:</span>
<strong>{field.executingCount}</strong>
</div>
)}
{field.completedCount > 0 && (
<div className="flex justify-between text-green-600 dark:text-green-400">
<span>:</span>
<strong>{field.completedCount}</strong>
</div>
)}
</div>
{/* 最新决策 */}
{field.decisions.length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
<div className="text-xs text-muted-foreground mb-1">:</div>
<div className="text-xs font-medium line-clamp-2">
{field.decisions[0].title}
</div>
</div>
)}
</div>
</div>
);
})}
{/* 图例 */}
<div className="absolute bottom-4 right-4 bg-white/95 dark:bg-gray-800/95 rounded-lg shadow-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="text-xs font-medium mb-2"></div>
<div className="space-y-1.5 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500 dark:bg-red-600"></div>
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-500 dark:bg-blue-600"></div>
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-purple-500 dark:bg-purple-600"></div>
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500 dark:bg-green-600"></div>
<span></span>
</div>
</div>
</div>
</div>
{/* 地块列表 */}
<div className="mt-4 space-y-2 max-h-48 overflow-y-auto">
{fieldDecisions.map((field) => (
<div key={field.fieldId} className="flex items-center justify-between p-2 bg-muted hover:bg-accent transition-colors rounded">
<div className="flex items-center gap-2">
<MapPin className={`w-4 h-4 ${
field.urgentCount > 0 ? 'text-red-500 dark:text-red-400' :
field.generatedCount > 0 ? 'text-blue-500 dark:text-blue-400' :
field.executingCount > 0 ? 'text-purple-500 dark:text-purple-400' :
'text-green-500 dark:text-green-400'
}`} />
<div>
<div className="text-sm font-medium">{field.fieldName}</div>
<div className="text-xs text-muted-foreground">
{field.cropType} · {field.area}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs font-light">
{field.decisions.length}
</Badge>
{field.urgentCount > 0 && (
<Badge variant="outline" className="text-xs font-light bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800">
{field.urgentCount}
</Badge>
)}
</div>
</div>
))}
</div>
</Card>
);
}

View File

@@ -0,0 +1,86 @@
/**
* filekorolheader: 决策趋势图组件 - 决策生成与完成趋势分析
* 功能:趋势线图、数据可视化、时间序列展示
* 路径:/ai-crop-model/support/dashboard/components/DecisionTrends
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Calendar } from 'lucide-react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import { TrendData } from './aiDecisionDashboardReducer';
interface DecisionTrendsProps {
trendData: TrendData[];
}
export function DecisionTrends({ trendData }: DecisionTrendsProps) {
return (
<Card className="p-6 bg-card">
<div className="flex items-center justify-between mb-4">
<h3></h3>
<Badge variant="outline" className="font-light">
<Calendar className="w-3 h-3 mr-1" />
7
</Badge>
</div>
<div className="h-96">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={trendData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="date"
className="text-xs"
tick={{ fill: 'hsl(var(--muted-foreground))' }}
/>
<YAxis
className="text-xs"
tick={{ fill: 'hsl(var(--muted-foreground))' }}
/>
<RechartsTooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
labelStyle={{ color: 'hsl(var(--foreground))' }}
/>
<Legend
wrapperStyle={{
fontSize: '12px',
}}
/>
<Line
type="monotone"
dataKey="generated"
stroke="hsl(var(--chart-1))"
name="生成决策"
strokeWidth={2}
dot={{ r: 4, fill: 'hsl(var(--chart-1))' }}
activeDot={{ r: 6 }}
/>
<Line
type="monotone"
dataKey="completed"
stroke="hsl(var(--chart-2))"
name="完成决策"
strokeWidth={2}
dot={{ r: 4, fill: 'hsl(var(--chart-2))' }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</Card>
);
}

View File

@@ -0,0 +1,157 @@
/**
* filekorolheader: 最新决策建议组件 - 最新决策展示与详情
* 功能:决策列表、状态标识、优先级显示、详情展示
* 路径:/ai-crop-model/support/dashboard/components/LatestDecisions
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
Info,
Sparkles,
Droplets,
Bug,
Sprout,
Package,
CloudRain,
Layers,
Activity,
CheckCircle,
Clock,
MapPin,
Zap,
CircleDot,
} from 'lucide-react';
import { DecisionRecord } from './aiDecisionDashboardReducer';
interface LatestDecisionsProps {
latestDecisions: DecisionRecord[];
}
export function LatestDecisions({ latestDecisions }: LatestDecisionsProps) {
const getTypeBadge = (type: string) => {
const config = {
irrigation: { label: '灌溉', icon: Droplets, className: 'bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800' },
fertilizer: { label: '施肥', icon: Sprout, className: 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800' },
pesticide: { label: '打药', icon: Bug, className: 'bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800' },
harvest: { label: '收获', icon: Package, className: 'bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800' },
soil: { label: '土壤', icon: Layers, className: 'bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800' },
weather: { label: '气象', icon: CloudRain, className: 'bg-cyan-50 dark:bg-cyan-950 text-cyan-600 dark:text-cyan-400 border-cyan-200 dark:border-cyan-800' },
};
const { label, icon: Icon, className } = config[type as keyof typeof config];
return (
<Badge variant="outline" className={`font-light ${className}`}>
<Icon className="w-3 h-3 mr-1" />
{label}
</Badge>
);
};
const getPriorityBadge = (priority: string) => {
const config = {
urgent: { label: '紧急', className: 'bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800' },
high: { label: '高', className: 'bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800' },
medium: { label: '中', className: 'bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800' },
low: { label: '低', className: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800' },
};
const { label, className } = config[priority as keyof typeof config];
return <Badge variant="outline" className={`font-light ${className}`}>{label}</Badge>;
};
const getStatusBadge = (status: string) => {
const config = {
generated: { label: '已生成', icon: Sparkles, className: 'bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800' },
executing: { label: '执行中', icon: Activity, className: 'bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800' },
completed: { label: '已完成', icon: CheckCircle, className: 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800' },
expired: { label: '已过期', icon: Clock, className: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800' },
};
const { label, icon: Icon, className } = config[status as keyof typeof config];
return (
<Badge variant="outline" className={`font-light ${className}`}>
<Icon className="w-3 h-3 mr-1" />
{label}
</Badge>
);
};
return (
<Card className="p-6 bg-card">
<div className="flex items-center justify-between mb-4">
<h3></h3>
<Badge variant="outline" className="bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 font-light">
<Sparkles className="w-3 h-3 mr-1" />
5
</Badge>
</div>
{latestDecisions.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<Info className="w-12 h-12 mx-auto mb-3 opacity-50" />
<div></div>
</div>
) : (
<div className="space-y-4">
{latestDecisions.map((decision, index) => (
<Card key={decision.id} className="p-5 hover:shadow-md transition-shadow border-l-4 bg-card"
style={{
borderLeftColor: decision.priority === 'urgent' ? '#ef4444' :
decision.priority === 'high' ? '#f59e0b' :
decision.priority === 'medium' ? '#eab308' : '#9ca3af'
}}>
<div className="flex items-start">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2 flex-wrap">
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 font-light">
<CircleDot className="w-3 h-3 mr-1" />
#{index + 1}
</Badge>
{getTypeBadge(decision.type)}
{getPriorityBadge(decision.priority)}
{getStatusBadge(decision.status)}
</div>
<h4 className="mb-2">{decision.title}</h4>
<p className="text-sm text-muted-foreground mb-3">
{decision.description}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div>
<div className="text-muted-foreground mb-1"></div>
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="font-medium">{decision.fieldName}</span>
</div>
</div>
<div>
<div className="text-muted-foreground mb-1"></div>
<div className="flex items-center gap-1">
<Sprout className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="font-medium">{decision.cropType}</span>
</div>
</div>
<div>
<div className="text-muted-foreground mb-1"></div>
<div className="flex items-center gap-1">
<Zap className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
<span className="font-medium">{(decision.confidence * 100).toFixed(0)}%</span>
</div>
</div>
<div>
<div className="text-muted-foreground mb-1"></div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4 text-muted-foreground" />
<span className="font-medium">{decision.createdAt}</span>
</div>
</div>
</div>
</div>
</div>
</Card>
))}
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,101 @@
/**
* filekorolheader: 统计卡片组件 - AI决策统计数据展示
* 功能:总决策数、状态分布、优先级统计、置信度展示
* 路径:/ai-crop-model/support/dashboard/components/StatisticsCards
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
LayoutDashboard,
Sparkles,
Activity,
CheckCircle,
AlertCircle,
Zap,
} from 'lucide-react';
import { DecisionStats } from './aiDecisionDashboardReducer';
interface StatisticsCardsProps {
stats: DecisionStats;
}
export function StatisticsCards({ stats }: StatisticsCardsProps) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{/* 总决策数 */}
<Card className="p-4 bg-card hover:bg-muted transition-colors">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<LayoutDashboard className="w-4 h-4 text-muted-foreground" />
</div>
<div className="text-2xl font-bold">{stats.total}</div>
<div className="text-xs text-muted-foreground mt-1">
</div>
</Card>
{/* 已生成 */}
<Card className="p-4 bg-card hover:bg-muted transition-colors">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<Sparkles className="w-4 h-4 text-blue-500 dark:text-blue-400" />
</div>
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">{stats.generated}</div>
<div className="text-xs text-muted-foreground mt-1">
</div>
</Card>
{/* 执行中 */}
<Card className="p-4 bg-card hover:bg-muted transition-colors">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<Activity className="w-4 h-4 text-purple-500 dark:text-purple-400" />
</div>
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">{stats.executing}</div>
<div className="text-xs text-muted-foreground mt-1">
</div>
</Card>
{/* 已完成 */}
<Card className="p-4 bg-card hover:bg-muted transition-colors">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<CheckCircle className="w-4 h-4 text-green-500 dark:text-green-400" />
</div>
<div className="text-2xl font-bold text-green-600 dark:text-green-400">{stats.completed}</div>
<div className="text-xs text-muted-foreground mt-1">
</div>
</Card>
{/* 紧急决策 */}
<Card className="p-4 bg-card hover:bg-muted transition-colors">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<AlertCircle className="w-4 h-4 text-red-500 dark:text-red-400" />
</div>
<div className="text-2xl font-bold text-red-600 dark:text-red-400">{stats.urgent}</div>
<div className="text-xs text-muted-foreground mt-1">
</div>
</Card>
{/* 平均置信度 */}
<Card className="p-4 bg-card hover:bg-muted transition-colors">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<Zap className="w-4 h-4 text-yellow-500 dark:text-yellow-400" />
</div>
<div className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
{(stats.avgConfidence * 100).toFixed(0)}%
</div>
<div className="text-xs text-muted-foreground mt-1">
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,446 @@
/**
* filekorolheader: AI决策看板状态管理 - 决策数据状态管理核心
* 功能:决策数据管理、统计计算、状态更新、本地存储同步
* 路径:/ai-crop-model/support/dashboard/components/aiDecisionDashboardReducer
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理模式
*/
// 决策类型
export type DecisionType = 'irrigation' | 'fertilizer' | 'pesticide' | 'harvest' | 'soil' | 'weather';
// 决策状态
export type DecisionStatus = 'generated' | 'executing' | 'completed' | 'expired';
// 决策优先级
export type DecisionPriority = 'urgent' | 'high' | 'medium' | 'low';
// 决策记录
export interface DecisionRecord {
id: string;
type: DecisionType;
title: string;
description: string;
status: DecisionStatus;
priority: DecisionPriority;
fieldId: string;
fieldName: string;
fieldArea: number;
cropType: string;
confidence: number;
createdAt: string;
updatedAt: string;
dueDate: string;
location: {
lat: number;
lng: number;
};
modelVersion?: string;
ruleCount?: number;
executedAt?: string;
executedBy?: string;
completedAt?: string;
}
// 地块决策信息
export interface FieldDecisionInfo {
fieldId: string;
fieldName: string;
location: {
lat: number;
lng: number;
};
area: number;
cropType: string;
decisions: DecisionRecord[];
urgentCount: number;
generatedCount: number;
executingCount: number;
completedCount: number;
}
// 统计数据
export interface DecisionStats {
total: number;
generated: number;
executing: number;
completed: number;
urgent: number;
avgConfidence: number;
}
// 趋势数据
export interface TrendData {
date: string;
generated: number;
completed: number;
}
// 状态接口
export interface AIDecisionDashboardState {
decisions: DecisionRecord[];
fieldDecisions: FieldDecisionInfo[];
stats: DecisionStats;
trendData: TrendData[];
latestDecisions: DecisionRecord[];
lastUpdated: string;
}
// Action类型
export type AIDecisionDashboardAction =
| { type: 'SET_DECISIONS'; payload: DecisionRecord[] }
| { type: 'ADD_DECISION'; payload: DecisionRecord }
| { type: 'UPDATE_DECISION'; payload: { id: string; updates: Partial<DecisionRecord> } }
| { type: 'DELETE_DECISION'; payload: string }
| { type: 'REFRESH_DATA' }
| { type: 'LOAD_FROM_STORAGE'; payload: Partial<AIDecisionDashboardState> };
// 初始决策数据
const initialDecisions: DecisionRecord[] = [
{
id: 'dec_001',
type: 'irrigation',
title: '1号大棚番茄开花期灌溉',
description: '土壤湿度35%低于最佳湿度建议立即灌溉120升',
status: 'generated',
priority: 'urgent',
fieldId: 'field_001',
fieldName: '1号大棚',
fieldArea: 2.5,
cropType: '番茄',
confidence: 0.89,
createdAt: '2024-10-23 14:30',
updatedAt: '2024-10-23 14:30',
dueDate: '2024-10-23 18:00',
location: { lat: 39.9042, lng: 116.4074 },
modelVersion: 'v2.1.3',
ruleCount: 2,
},
{
id: 'dec_002',
type: 'pesticide',
title: '2号大棚早疫病防治',
description: '检测到早疫病轻度感染,建议使用生物防治剂',
status: 'executing',
priority: 'high',
fieldId: 'field_002',
fieldName: '2号大棚',
fieldArea: 3.0,
cropType: '番茄',
confidence: 0.87,
createdAt: '2024-10-23 10:15',
updatedAt: '2024-10-23 11:00',
dueDate: '2024-10-23 17:00',
executedAt: '2024-10-23 11:00',
executedBy: '王五',
location: { lat: 39.9142, lng: 116.4174 },
modelVersion: 'v3.2.1',
ruleCount: 1,
},
{
id: 'dec_003',
type: 'fertilizer',
title: '3号地块小麦追肥',
description: '结果期营养需求增加,建议施用复合肥',
status: 'executing',
priority: 'medium',
fieldId: 'field_003',
fieldName: '3号地块',
fieldArea: 5.0,
cropType: '小麦',
confidence: 0.92,
createdAt: '2024-10-23 09:30',
updatedAt: '2024-10-23 10:00',
dueDate: '2024-10-24 12:00',
executedAt: '2024-10-23 10:00',
executedBy: '李四',
location: { lat: 39.8942, lng: 116.3974 },
modelVersion: 'v2.0.5',
ruleCount: 3,
},
{
id: 'dec_004',
type: 'soil',
title: '4号地块土壤改良',
description: 'pH值偏低建议施用石灰调节土壤酸碱度',
status: 'completed',
priority: 'low',
fieldId: 'field_004',
fieldName: '4号地块',
fieldArea: 4.2,
cropType: '玉米',
confidence: 0.85,
createdAt: '2024-10-22 15:00',
updatedAt: '2024-10-22 16:30',
dueDate: '2024-10-25 12:00',
executedAt: '2024-10-22 15:30',
executedBy: '张三',
completedAt: '2024-10-22 16:30',
location: { lat: 39.8842, lng: 116.3874 },
modelVersion: 'v1.8.2',
ruleCount: 1,
},
{
id: 'dec_005',
type: 'weather',
title: '5号大棚温度调控',
description: '预计晚间温度降至12℃建议提前加温',
status: 'generated',
priority: 'high',
fieldId: 'field_005',
fieldName: '5号大棚',
fieldArea: 2.8,
cropType: '黄瓜',
confidence: 0.91,
createdAt: '2024-10-23 16:00',
updatedAt: '2024-10-23 16:00',
dueDate: '2024-10-23 20:00',
location: { lat: 39.9242, lng: 116.4274 },
modelVersion: 'v2.3.1',
ruleCount: 2,
},
{
id: 'dec_006',
type: 'harvest',
title: '6号地块水稻收获',
description: '水稻已达成熟期建议3天内完成收获',
status: 'generated',
priority: 'urgent',
fieldId: 'field_006',
fieldName: '6号地块',
fieldArea: 8.0,
cropType: '水稻',
confidence: 0.94,
createdAt: '2024-10-23 08:00',
updatedAt: '2024-10-23 08:00',
dueDate: '2024-10-26 18:00',
location: { lat: 39.9342, lng: 116.3874 },
modelVersion: 'v2.5.0',
ruleCount: 3,
},
{
id: 'dec_007',
type: 'irrigation',
title: '7号大棚茄子补水',
description: '连续3天无降雨土壤湿度偏低',
status: 'completed',
priority: 'medium',
fieldId: 'field_007',
fieldName: '7号大棚',
fieldArea: 2.2,
cropType: '茄子',
confidence: 0.86,
createdAt: '2024-10-22 18:00',
updatedAt: '2024-10-23 09:00',
dueDate: '2024-10-23 12:00',
executedAt: '2024-10-22 19:00',
executedBy: '赵六',
completedAt: '2024-10-23 09:00',
location: { lat: 39.8742, lng: 116.4174 },
modelVersion: 'v2.1.3',
ruleCount: 2,
},
];
// 初始趋势数据
const initialTrendData: TrendData[] = [
{ date: '10-17', generated: 8, completed: 5 },
{ date: '10-18', generated: 12, completed: 8 },
{ date: '10-19', generated: 10, completed: 9 },
{ date: '10-20', generated: 15, completed: 11 },
{ date: '10-21', generated: 13, completed: 10 },
{ date: '10-22', generated: 11, completed: 9 },
{ date: '10-23', generated: 14, completed: 7 },
];
// 计算统计数据
const calculateStats = (decisions: DecisionRecord[]): DecisionStats => {
return {
total: decisions.length,
generated: decisions.filter(d => d.status === 'generated').length,
executing: decisions.filter(d => d.status === 'executing').length,
completed: decisions.filter(d => d.status === 'completed').length,
urgent: decisions.filter(d => d.priority === 'urgent').length,
avgConfidence: decisions.reduce((sum, d) => sum + d.confidence, 0) / decisions.length,
};
};
// 计算地块决策信息
const calculateFieldDecisions = (decisions: DecisionRecord[]): FieldDecisionInfo[] => {
const fieldDecisionMap = new Map<string, FieldDecisionInfo>();
decisions.forEach(decision => {
if (!fieldDecisionMap.has(decision.fieldId)) {
fieldDecisionMap.set(decision.fieldId, {
fieldId: decision.fieldId,
fieldName: decision.fieldName,
location: decision.location,
area: decision.fieldArea,
cropType: decision.cropType,
decisions: [],
urgentCount: 0,
generatedCount: 0,
executingCount: 0,
completedCount: 0,
});
}
const fieldInfo = fieldDecisionMap.get(decision.fieldId)!;
fieldInfo.decisions.push(decision);
if (decision.priority === 'urgent') fieldInfo.urgentCount++;
if (decision.status === 'generated') fieldInfo.generatedCount++;
if (decision.status === 'executing') fieldInfo.executingCount++;
if (decision.status === 'completed') fieldInfo.completedCount++;
});
return Array.from(fieldDecisionMap.values());
};
// 计算最新决策
const calculateLatestDecisions = (decisions: DecisionRecord[]): DecisionRecord[] => {
return [...decisions]
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, 5);
};
// 保存到本地存储
const saveToStorage = (state: AIDecisionDashboardState) => {
try {
localStorage.setItem('ai-decision-dashboard', JSON.stringify({
decisions: state.decisions,
lastUpdated: state.lastUpdated,
}));
} catch (error) {
console.warn('Failed to save to localStorage:', error);
}
};
// 从本地存储加载
const loadFromStorage = () => {
try {
const stored = localStorage.getItem('ai-decision-dashboard');
if (stored) {
const data = JSON.parse(stored);
return {
decisions: data.decisions || initialDecisions,
lastUpdated: data.lastUpdated || new Date().toISOString(),
};
}
} catch (error) {
console.warn('Failed to load from localStorage:', error);
}
return null;
};
// 计算派生状态
const calculateDerivedState = (decisions: DecisionRecord[]) => {
const stats = calculateStats(decisions);
const fieldDecisions = calculateFieldDecisions(decisions);
const latestDecisions = calculateLatestDecisions(decisions);
return {
stats,
fieldDecisions,
latestDecisions,
trendData: initialTrendData,
};
};
// 初始状态
export const initialState: AIDecisionDashboardState = (() => {
const stored = loadFromStorage();
const decisions = stored?.decisions || initialDecisions;
const derivedState = calculateDerivedState(decisions);
return {
decisions,
...derivedState,
lastUpdated: stored?.lastUpdated || new Date().toISOString(),
};
})();
// Reducer
export function AIDecisionDashboardReducer(
state: AIDecisionDashboardState,
action: AIDecisionDashboardAction
): AIDecisionDashboardState {
switch (action.type) {
case 'SET_DECISIONS': {
const derivedState = calculateDerivedState(action.payload);
const newState = {
...state,
decisions: action.payload,
...derivedState,
lastUpdated: new Date().toISOString(),
};
saveToStorage(newState);
return newState;
}
case 'ADD_DECISION': {
const newDecisions = [...state.decisions, action.payload];
const derivedState = calculateDerivedState(newDecisions);
const newState = {
...state,
decisions: newDecisions,
...derivedState,
lastUpdated: new Date().toISOString(),
};
saveToStorage(newState);
return newState;
}
case 'UPDATE_DECISION': {
const newDecisions = state.decisions.map(decision =>
decision.id === action.payload.id
? { ...decision, ...action.payload.updates }
: decision
);
const derivedState = calculateDerivedState(newDecisions);
const newState = {
...state,
decisions: newDecisions,
...derivedState,
lastUpdated: new Date().toISOString(),
};
saveToStorage(newState);
return newState;
}
case 'DELETE_DECISION': {
const newDecisions = state.decisions.filter(decision => decision.id !== action.payload);
const derivedState = calculateDerivedState(newDecisions);
const newState = {
...state,
decisions: newDecisions,
...derivedState,
lastUpdated: new Date().toISOString(),
};
saveToStorage(newState);
return newState;
}
case 'REFRESH_DATA': {
const derivedState = calculateDerivedState(state.decisions);
const newState = {
...state,
...derivedState,
lastUpdated: new Date().toISOString(),
};
saveToStorage(newState);
return newState;
}
case 'LOAD_FROM_STORAGE': {
const decisions = action.payload.decisions || state.decisions;
const derivedState = calculateDerivedState(decisions);
return {
...state,
decisions,
...derivedState,
lastUpdated: action.payload.lastUpdated || state.lastUpdated,
};
}
default:
return state;
}
}

View File

@@ -0,0 +1,104 @@
/**
* filekorolheader: AI决策看板 - 智能决策生成与执行监控中心
* 功能:决策统计展示、地图可视化、趋势分析、决策详情查看、状态管理
* 路径:/ai-crop-model/support/dashboard
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式
*/
'use client';
import { useReducer } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import {
LayoutDashboard,
RefreshCw,
CheckCircle,
AlertCircle,
TrendingUp,
Droplets,
Bug,
Sprout,
Package,
CloudRain,
Layers,
Activity,
Clock,
Info,
Sparkles,
Calendar,
MapPin,
Zap,
CircleDot,
Map as MapIcon,
} from 'lucide-react';
import { AIDecisionDashboardReducer, initialState, AIDecisionDashboardState, AIDecisionDashboardAction } from './components/aiDecisionDashboardReducer';
import { StatisticsCards } from './components/StatisticsCards';
import { DecisionMap } from './components/DecisionMap';
import { DecisionTrends } from './components/DecisionTrends';
import { LatestDecisions } from './components/LatestDecisions';
export default function DashboardPage() {
const [state, dispatch] = useReducer(AIDecisionDashboardReducer, initialState);
const handleRefresh = () => {
dispatch({ type: 'REFRESH_DATA' });
toast.success('数据已刷新');
};
return (
<div className="space-y-6">
{/* 页面标题 */}
<Card className="p-6 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950 dark:to-emerald-950 border-green-200 dark:border-green-800">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<LayoutDashboard className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="mb-2">AI决策看板</h2>
<p className="text-sm text-muted-foreground mb-3">
AI决策生成与执行统计
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="bg-white dark:bg-gray-800">
<Sparkles className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white dark:bg-gray-800">
<Activity className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white dark:bg-gray-800">
<TrendingUp className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white dark:bg-gray-800">
<MapIcon className="w-3 h-3 mr-1" />
</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleRefresh}>
<RefreshCw className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
</Card>
{/* 核心统计卡片 */}
<StatisticsCards stats={state.stats} />
{/* 地图可视化 + 决策趋势图 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<DecisionMap fieldDecisions={state.fieldDecisions} />
<DecisionTrends trendData={state.trendData} />
</div>
{/* 最新决策建议 */}
<LatestDecisions latestDecisions={state.latestDecisions} />
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function SupportPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-xl font-semibold"></h2>
<div className="p-3 bg-muted rounded-lg mt-3">
<p className="text-sm">
<strong></strong> /ai-crop-model/support
</p>
</div>
</Card>
</div>
);
}