生产管理系统 - 应用生成、调度管理

This commit is contained in:
2025-11-01 16:26:26 +08:00
parent 624fc38b21
commit c942a2ce07
8 changed files with 3433 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,303 @@
/**
* filekorolheader: 模型应用中心生成页面 - AI模型应用生成管理
* 功能:应用生成、应用管理、应用运行、结果展示
* 路径:/ai-crop-model/model-application/generation
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式
*/
'use client'; 'use client';
import { useReducer } from 'react';
import { Card } from '@/components/ui/card'; 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() { 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card className="p-6"> {/* 页面标题和描述 */}
<h2 className="text-xl font-semibold"></h2> <Card className="p-6 bg-gradient-to-r from-blue-50 to-cyan-50 border-blue-200">
<div className="p-3 bg-muted rounded-lg mt-3"> <div className="flex items-center gap-3 mb-4">
<p className="text-sm"> <Rocket className="w-8 h-8 text-blue-600" />
<strong></strong> /ai-crop-model/model-application/generation <div>
</p> <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> </div>
</Card> </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> </div>
); );
} }

View File

@@ -0,0 +1,76 @@
/**
* filekorolheader: AI模型类型定义 - 模型应用中心相关类型
* 功能:应用类型、执行历史、调度任务等类型定义
* 路径:/types/ai-model.ts
* 规范遵循crop-x/docs/开发项目规范.md使用TypeScript类型安全
*/
export type ApplicationType = '智能灌溉' | '病虫害预警' | '施肥推荐' | '产量预测' | '生长监测' | '其他';
export type ApplicationStatus = '运行中' | '已停止' | '故障';
export type TriggerType = '定时触发' | '周期触发' | '事件触发' | '手动触发';
export type TaskStatus = '等待中' | '运行中' | '已完成' | '失败' | '已取消';
export type Priority = '高' | '中' | '低';
export type OutputFormat = '折线图' | '饼状图' | '表格' | '文字';
export interface Application {
id: string;
name: string;
type: ApplicationType;
description: string;
modelName: string;
modelVersion: string;
inputConfig: {
fields: string[];
};
outputConfig: {
format: OutputFormat;
};
status: ApplicationStatus;
createTime: string;
lastRunTime?: string;
runCount: number;
successRate: number;
avgExecutionTime: number;
}
export interface ExecutionHistory {
id: string;
executeTime: string;
status: TaskStatus;
executionTime: number;
result?: string;
errorMessage?: string;
}
export interface ScheduleTask {
id: string;
taskNo: string;
applicationName: string;
triggerType: TriggerType;
priority: Priority;
scheduledTime: string;
actualStartTime?: string;
actualEndTime?: string;
status: TaskStatus;
executionTime?: number;
retryCount: number;
maxRetry: number;
errorMessage?: string;
result?: string;
executionHistory?: ExecutionHistory[];
}
export interface TaskQueue {
waiting: number;
running: number;
completed: number;
failed: number;
}
export interface ModelService {
id: string;
name: string;
version: string;
type: string;
status: string;
}