子仓库提交

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff