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