生产管理系统 - 业务融合,决策模拟,决策日志提交

This commit is contained in:
2025-11-03 10:31:39 +08:00
parent c942a2ce07
commit fdeb455e47
16 changed files with 5377 additions and 39 deletions

View File

@@ -5,14 +5,7 @@ import { Card } from '@/components/ui/card';
export default function DataCenterPage() {
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/data-center
</p>
</div>
</Card>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1042,12 +1042,12 @@ const aiCropModel = {
icon: <Brain className="w-4 h-4" />,
items: [
{
title: "融合决策",
title: "业务融合",
url: "/ai-crop-model/decision/fusion",
isActive: false
},
{
title: "决策仿真",
title: "决策模拟",
url: "/ai-crop-model/decision/simulation",
isActive: false
},

View File

@@ -503,10 +503,10 @@ export function AIBusinessFusion({ activePath }: AIBusinessFusionProps) {
const getLevelBadge = (level: DecisionLevel) => {
const config = {
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' },
critical: { label: '紧急', className: 'bg-red-50 text-red-700 border-red-200' },
important: { label: '重要', className: 'bg-amber-50 text-amber-700 border-amber-200' },
normal: { label: '一般', className: 'bg-sky-50 text-sky-700 border-sky-200' },
suggestion: { label: '建议', className: 'bg-emerald-50 text-emerald-700 border-emerald-200' },
};
const { label, className } = config[level];
return <Badge variant="outline" className={className}>{label}</Badge>;
@@ -514,8 +514,8 @@ export function AIBusinessFusion({ activePath }: AIBusinessFusionProps) {
const getSourceBadge = (source: DecisionSource) => {
const config = {
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 },
auto: { label: '自动生成', className: 'bg-emerald-50 text-emerald-700 border-emerald-200', icon: Brain },
manual: { label: '手动添加', className: 'bg-sky-50 text-sky-700 border-sky-200', icon: User },
};
const { label, className, icon: Icon } = config[source];
return (
@@ -528,8 +528,8 @@ export function AIBusinessFusion({ activePath }: AIBusinessFusionProps) {
const getExecutionModeBadge = (mode: ExecutionMode) => {
const config = {
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 },
manual: { label: '手动执行', className: 'bg-sky-50 text-sky-700 border-sky-200', icon: User },
auto: { label: '自动执行', className: 'bg-emerald-50 text-emerald-700 border-emerald-200', icon: Zap },
};
const { label, className, icon: Icon } = config[mode];
return (