生产管理系统 决策看板 决策详情开发

This commit is contained in:
2025-11-03 15:21:11 +08:00
parent 9898a5ea38
commit 45c2309662
8 changed files with 2742 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff