生产管理系统 决策看板 决策详情开发
This commit is contained in:
2
crop-x/next-env.d.ts
vendored
2
crop-x/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* filekorolheader: 地块决策分布地图组件 - 地理位置决策可视化
|
||||
* 功能:地块标记、状态可视化、悬浮详情、图例说明
|
||||
* 路径:/ai-crop-model/support/dashboard/components/DecisionMap
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||
*/
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { MapPin, Map as MapIcon } from 'lucide-react';
|
||||
import { FieldDecisionInfo } from './aiDecisionDashboardReducer';
|
||||
|
||||
interface DecisionMapProps {
|
||||
fieldDecisions: FieldDecisionInfo[];
|
||||
}
|
||||
|
||||
export function DecisionMap({ fieldDecisions }: DecisionMapProps) {
|
||||
return (
|
||||
<Card className="p-6 bg-card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>地块决策分布地图</h3>
|
||||
<Badge variant="outline" className="font-light">
|
||||
<MapIcon className="w-3 h-3 mr-1" />
|
||||
{fieldDecisions.length}个地块
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 模拟地图区域 */}
|
||||
<div className="relative h-96 bg-gradient-to-br from-green-50 dark:from-green-950 to-blue-50 dark:to-blue-950 rounded-lg border-2 border-green-200 dark:border-green-800 overflow-hidden">
|
||||
{/* 地图背景 */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="w-full h-full" style={{
|
||||
backgroundImage: 'repeating-linear-gradient(0deg, #10b981 0px, #10b981 1px, transparent 1px, transparent 20px), repeating-linear-gradient(90deg, #10b981 0px, #10b981 1px, transparent 1px, transparent 20px)',
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
{/* 地块标记点 */}
|
||||
{fieldDecisions.map((field, index) => {
|
||||
// 计算标记点位置(模拟分布)
|
||||
const positions = [
|
||||
{ top: '15%', left: '20%' },
|
||||
{ top: '25%', left: '65%' },
|
||||
{ top: '45%', left: '30%' },
|
||||
{ top: '55%', left: '75%' },
|
||||
{ top: '70%', left: '45%' },
|
||||
{ top: '35%', left: '85%' },
|
||||
{ top: '80%', left: '25%' },
|
||||
];
|
||||
const position = positions[index] || { top: '50%', left: '50%' };
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.fieldId}
|
||||
className="absolute transform -translate-x-1/2 -translate-y-1/2 group cursor-pointer"
|
||||
style={position}
|
||||
>
|
||||
{/* 标记点 */}
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center shadow-lg transition-transform hover:scale-125 ${
|
||||
field.urgentCount > 0 ? 'bg-red-500 dark:bg-red-600' :
|
||||
field.generatedCount > 0 ? 'bg-blue-500 dark:bg-blue-600' :
|
||||
field.executingCount > 0 ? 'bg-purple-500 dark:bg-purple-600' :
|
||||
'bg-green-500 dark:bg-green-600'
|
||||
}`}>
|
||||
<MapPin className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
|
||||
{/* 决策数量标记 */}
|
||||
{field.decisions.length > 0 && (
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-yellow-500 dark:bg-yellow-600 text-white rounded-full flex items-center justify-center text-xs font-bold shadow-lg">
|
||||
{field.decisions.length}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 悬浮信息卡片 */}
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-xl p-4 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10 border-2 border-green-200 dark:border-green-800">
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<MapPin className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
<strong>{field.fieldName}</strong>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{field.cropType} · {field.area}亩
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">总决策:</span>
|
||||
<strong>{field.decisions.length}条</strong>
|
||||
</div>
|
||||
{field.urgentCount > 0 && (
|
||||
<div className="flex justify-between text-red-600 dark:text-red-400">
|
||||
<span>紧急:</span>
|
||||
<strong>{field.urgentCount}条</strong>
|
||||
</div>
|
||||
)}
|
||||
{field.generatedCount > 0 && (
|
||||
<div className="flex justify-between text-blue-600 dark:text-blue-400">
|
||||
<span>已生成:</span>
|
||||
<strong>{field.generatedCount}条</strong>
|
||||
</div>
|
||||
)}
|
||||
{field.executingCount > 0 && (
|
||||
<div className="flex justify-between text-purple-600 dark:text-purple-400">
|
||||
<span>执行中:</span>
|
||||
<strong>{field.executingCount}条</strong>
|
||||
</div>
|
||||
)}
|
||||
{field.completedCount > 0 && (
|
||||
<div className="flex justify-between text-green-600 dark:text-green-400">
|
||||
<span>已完成:</span>
|
||||
<strong>{field.completedCount}条</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 最新决策 */}
|
||||
{field.decisions.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-muted-foreground mb-1">最新决策:</div>
|
||||
<div className="text-xs font-medium line-clamp-2">
|
||||
{field.decisions[0].title}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 图例 */}
|
||||
<div className="absolute bottom-4 right-4 bg-white/95 dark:bg-gray-800/95 rounded-lg shadow-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs font-medium mb-2">状态图例</div>
|
||||
<div className="space-y-1.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500 dark:bg-red-600"></div>
|
||||
<span>有紧急决策</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500 dark:bg-blue-600"></div>
|
||||
<span>有待执行决策</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-purple-500 dark:bg-purple-600"></div>
|
||||
<span>有执行中决策</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500 dark:bg-green-600"></div>
|
||||
<span>全部完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 地块列表 */}
|
||||
<div className="mt-4 space-y-2 max-h-48 overflow-y-auto">
|
||||
{fieldDecisions.map((field) => (
|
||||
<div key={field.fieldId} className="flex items-center justify-between p-2 bg-muted hover:bg-accent transition-colors rounded">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className={`w-4 h-4 ${
|
||||
field.urgentCount > 0 ? 'text-red-500 dark:text-red-400' :
|
||||
field.generatedCount > 0 ? 'text-blue-500 dark:text-blue-400' :
|
||||
field.executingCount > 0 ? 'text-purple-500 dark:text-purple-400' :
|
||||
'text-green-500 dark:text-green-400'
|
||||
}`} />
|
||||
<div>
|
||||
<div className="text-sm font-medium">{field.fieldName}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{field.cropType} · {field.area}亩
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs font-light">
|
||||
{field.decisions.length}条决策
|
||||
</Badge>
|
||||
{field.urgentCount > 0 && (
|
||||
<Badge variant="outline" className="text-xs font-light bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800">
|
||||
{field.urgentCount}紧急
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* filekorolheader: 决策趋势图组件 - 决策生成与完成趋势分析
|
||||
* 功能:趋势线图、数据可视化、时间序列展示
|
||||
* 路径:/ai-crop-model/support/dashboard/components/DecisionTrends
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||
*/
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Calendar } from 'lucide-react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip as RechartsTooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { TrendData } from './aiDecisionDashboardReducer';
|
||||
|
||||
interface DecisionTrendsProps {
|
||||
trendData: TrendData[];
|
||||
}
|
||||
|
||||
export function DecisionTrends({ trendData }: DecisionTrendsProps) {
|
||||
return (
|
||||
<Card className="p-6 bg-card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>决策生成与完成趋势</h3>
|
||||
<Badge variant="outline" className="font-light">
|
||||
<Calendar className="w-3 h-3 mr-1" />
|
||||
近7天
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="h-96">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={trendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
className="text-xs"
|
||||
tick={{ fill: 'hsl(var(--muted-foreground))' }}
|
||||
/>
|
||||
<YAxis
|
||||
className="text-xs"
|
||||
tick={{ fill: 'hsl(var(--muted-foreground))' }}
|
||||
/>
|
||||
<RechartsTooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
labelStyle={{ color: 'hsl(var(--foreground))' }}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{
|
||||
fontSize: '12px',
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="generated"
|
||||
stroke="hsl(var(--chart-1))"
|
||||
name="生成决策"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: 'hsl(var(--chart-1))' }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="completed"
|
||||
stroke="hsl(var(--chart-2))"
|
||||
name="完成决策"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: 'hsl(var(--chart-2))' }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* filekorolheader: 最新决策建议组件 - 最新决策展示与详情
|
||||
* 功能:决策列表、状态标识、优先级显示、详情展示
|
||||
* 路径:/ai-crop-model/support/dashboard/components/LatestDecisions
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||
*/
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Info,
|
||||
Sparkles,
|
||||
Droplets,
|
||||
Bug,
|
||||
Sprout,
|
||||
Package,
|
||||
CloudRain,
|
||||
Layers,
|
||||
Activity,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
MapPin,
|
||||
Zap,
|
||||
CircleDot,
|
||||
} from 'lucide-react';
|
||||
import { DecisionRecord } from './aiDecisionDashboardReducer';
|
||||
|
||||
interface LatestDecisionsProps {
|
||||
latestDecisions: DecisionRecord[];
|
||||
}
|
||||
|
||||
export function LatestDecisions({ latestDecisions }: LatestDecisionsProps) {
|
||||
const getTypeBadge = (type: string) => {
|
||||
const config = {
|
||||
irrigation: { label: '灌溉', icon: Droplets, className: 'bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800' },
|
||||
fertilizer: { label: '施肥', icon: Sprout, className: 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800' },
|
||||
pesticide: { label: '打药', icon: Bug, className: 'bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800' },
|
||||
harvest: { label: '收获', icon: Package, className: 'bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800' },
|
||||
soil: { label: '土壤', icon: Layers, className: 'bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800' },
|
||||
weather: { label: '气象', icon: CloudRain, className: 'bg-cyan-50 dark:bg-cyan-950 text-cyan-600 dark:text-cyan-400 border-cyan-200 dark:border-cyan-800' },
|
||||
};
|
||||
const { label, icon: Icon, className } = config[type as keyof typeof config];
|
||||
return (
|
||||
<Badge variant="outline" className={`font-light ${className}`}>
|
||||
<Icon className="w-3 h-3 mr-1" />
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority: string) => {
|
||||
const config = {
|
||||
urgent: { label: '紧急', className: 'bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800' },
|
||||
high: { label: '高', className: 'bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800' },
|
||||
medium: { label: '中', className: 'bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800' },
|
||||
low: { label: '低', className: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800' },
|
||||
};
|
||||
const { label, className } = config[priority as keyof typeof config];
|
||||
return <Badge variant="outline" className={`font-light ${className}`}>{label}</Badge>;
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const config = {
|
||||
generated: { label: '已生成', icon: Sparkles, className: 'bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800' },
|
||||
executing: { label: '执行中', icon: Activity, className: 'bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800' },
|
||||
completed: { label: '已完成', icon: CheckCircle, className: 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800' },
|
||||
expired: { label: '已过期', icon: Clock, className: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800' },
|
||||
};
|
||||
const { label, icon: Icon, className } = config[status as keyof typeof config];
|
||||
return (
|
||||
<Badge variant="outline" className={`font-light ${className}`}>
|
||||
<Icon className="w-3 h-3 mr-1" />
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6 bg-card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>最新决策建议</h3>
|
||||
<Badge variant="outline" className="bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 font-light">
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
最新5条
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{latestDecisions.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<Info className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<div>暂无决策建议</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{latestDecisions.map((decision, index) => (
|
||||
<Card key={decision.id} className="p-5 hover:shadow-md transition-shadow border-l-4 bg-card"
|
||||
style={{
|
||||
borderLeftColor: decision.priority === 'urgent' ? '#ef4444' :
|
||||
decision.priority === 'high' ? '#f59e0b' :
|
||||
decision.priority === 'medium' ? '#eab308' : '#9ca3af'
|
||||
}}>
|
||||
<div className="flex items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 font-light">
|
||||
<CircleDot className="w-3 h-3 mr-1" />
|
||||
#{index + 1}
|
||||
</Badge>
|
||||
{getTypeBadge(decision.type)}
|
||||
{getPriorityBadge(decision.priority)}
|
||||
{getStatusBadge(decision.status)}
|
||||
</div>
|
||||
|
||||
<h4 className="mb-2">{decision.title}</h4>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{decision.description}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">地块信息</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
<span className="font-medium">{decision.fieldName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">作物类型</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Sprout className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
<span className="font-medium">{decision.cropType}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">置信度</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="font-medium">{(decision.confidence * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">生成时间</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="font-medium">{decision.createdAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* filekorolheader: 统计卡片组件 - AI决策统计数据展示
|
||||
* 功能:总决策数、状态分布、优先级统计、置信度展示
|
||||
* 路径:/ai-crop-model/support/dashboard/components/StatisticsCards
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||
*/
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Sparkles,
|
||||
Activity,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { DecisionStats } from './aiDecisionDashboardReducer';
|
||||
|
||||
interface StatisticsCardsProps {
|
||||
stats: DecisionStats;
|
||||
}
|
||||
|
||||
export function StatisticsCards({ stats }: StatisticsCardsProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{/* 总决策数 */}
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">总决策数</div>
|
||||
<LayoutDashboard className="w-4 h-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats.total}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
全部决策
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 已生成 */}
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">已生成</div>
|
||||
<Sparkles className="w-4 h-4 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">{stats.generated}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
待执行
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 执行中 */}
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">执行中</div>
|
||||
<Activity className="w-4 h-4 text-purple-500 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">{stats.executing}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
正在执行
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 已完成 */}
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">已完成</div>
|
||||
<CheckCircle className="w-4 h-4 text-green-500 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">{stats.completed}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
执行完成
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 紧急决策 */}
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">紧急决策</div>
|
||||
<AlertCircle className="w-4 h-4 text-red-500 dark:text-red-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-red-600 dark:text-red-400">{stats.urgent}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
需优先处理
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 平均置信度 */}
|
||||
<Card className="p-4 bg-card hover:bg-muted transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">平均置信度</div>
|
||||
<Zap className="w-4 h-4 text-yellow-500 dark:text-yellow-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
||||
{(stats.avgConfidence * 100).toFixed(0)}%
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
决策准确性
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* filekorolheader: AI决策看板状态管理 - 决策数据状态管理核心
|
||||
* 功能:决策数据管理、统计计算、状态更新、本地存储同步
|
||||
* 路径:/ai-crop-model/support/dashboard/components/aiDecisionDashboardReducer
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理模式
|
||||
*/
|
||||
|
||||
// 决策类型
|
||||
export type DecisionType = 'irrigation' | 'fertilizer' | 'pesticide' | 'harvest' | 'soil' | 'weather';
|
||||
|
||||
// 决策状态
|
||||
export type DecisionStatus = 'generated' | 'executing' | 'completed' | 'expired';
|
||||
|
||||
// 决策优先级
|
||||
export type DecisionPriority = 'urgent' | 'high' | 'medium' | 'low';
|
||||
|
||||
// 决策记录
|
||||
export interface DecisionRecord {
|
||||
id: string;
|
||||
type: DecisionType;
|
||||
title: string;
|
||||
description: string;
|
||||
status: DecisionStatus;
|
||||
priority: DecisionPriority;
|
||||
fieldId: string;
|
||||
fieldName: string;
|
||||
fieldArea: number;
|
||||
cropType: string;
|
||||
confidence: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
dueDate: string;
|
||||
location: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
modelVersion?: string;
|
||||
ruleCount?: number;
|
||||
executedAt?: string;
|
||||
executedBy?: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
// 地块决策信息
|
||||
export interface FieldDecisionInfo {
|
||||
fieldId: string;
|
||||
fieldName: string;
|
||||
location: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
area: number;
|
||||
cropType: string;
|
||||
decisions: DecisionRecord[];
|
||||
urgentCount: number;
|
||||
generatedCount: number;
|
||||
executingCount: number;
|
||||
completedCount: number;
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
export interface DecisionStats {
|
||||
total: number;
|
||||
generated: number;
|
||||
executing: number;
|
||||
completed: number;
|
||||
urgent: number;
|
||||
avgConfidence: number;
|
||||
}
|
||||
|
||||
// 趋势数据
|
||||
export interface TrendData {
|
||||
date: string;
|
||||
generated: number;
|
||||
completed: number;
|
||||
}
|
||||
|
||||
// 状态接口
|
||||
export interface AIDecisionDashboardState {
|
||||
decisions: DecisionRecord[];
|
||||
fieldDecisions: FieldDecisionInfo[];
|
||||
stats: DecisionStats;
|
||||
trendData: TrendData[];
|
||||
latestDecisions: DecisionRecord[];
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
// Action类型
|
||||
export type AIDecisionDashboardAction =
|
||||
| { type: 'SET_DECISIONS'; payload: DecisionRecord[] }
|
||||
| { type: 'ADD_DECISION'; payload: DecisionRecord }
|
||||
| { type: 'UPDATE_DECISION'; payload: { id: string; updates: Partial<DecisionRecord> } }
|
||||
| { type: 'DELETE_DECISION'; payload: string }
|
||||
| { type: 'REFRESH_DATA' }
|
||||
| { type: 'LOAD_FROM_STORAGE'; payload: Partial<AIDecisionDashboardState> };
|
||||
|
||||
// 初始决策数据
|
||||
const initialDecisions: DecisionRecord[] = [
|
||||
{
|
||||
id: 'dec_001',
|
||||
type: 'irrigation',
|
||||
title: '1号大棚番茄开花期灌溉',
|
||||
description: '土壤湿度35%,低于最佳湿度,建议立即灌溉120升',
|
||||
status: 'generated',
|
||||
priority: 'urgent',
|
||||
fieldId: 'field_001',
|
||||
fieldName: '1号大棚',
|
||||
fieldArea: 2.5,
|
||||
cropType: '番茄',
|
||||
confidence: 0.89,
|
||||
createdAt: '2024-10-23 14:30',
|
||||
updatedAt: '2024-10-23 14:30',
|
||||
dueDate: '2024-10-23 18:00',
|
||||
location: { lat: 39.9042, lng: 116.4074 },
|
||||
modelVersion: 'v2.1.3',
|
||||
ruleCount: 2,
|
||||
},
|
||||
{
|
||||
id: 'dec_002',
|
||||
type: 'pesticide',
|
||||
title: '2号大棚早疫病防治',
|
||||
description: '检测到早疫病轻度感染,建议使用生物防治剂',
|
||||
status: 'executing',
|
||||
priority: 'high',
|
||||
fieldId: 'field_002',
|
||||
fieldName: '2号大棚',
|
||||
fieldArea: 3.0,
|
||||
cropType: '番茄',
|
||||
confidence: 0.87,
|
||||
createdAt: '2024-10-23 10:15',
|
||||
updatedAt: '2024-10-23 11:00',
|
||||
dueDate: '2024-10-23 17:00',
|
||||
executedAt: '2024-10-23 11:00',
|
||||
executedBy: '王五',
|
||||
location: { lat: 39.9142, lng: 116.4174 },
|
||||
modelVersion: 'v3.2.1',
|
||||
ruleCount: 1,
|
||||
},
|
||||
{
|
||||
id: 'dec_003',
|
||||
type: 'fertilizer',
|
||||
title: '3号地块小麦追肥',
|
||||
description: '结果期营养需求增加,建议施用复合肥',
|
||||
status: 'executing',
|
||||
priority: 'medium',
|
||||
fieldId: 'field_003',
|
||||
fieldName: '3号地块',
|
||||
fieldArea: 5.0,
|
||||
cropType: '小麦',
|
||||
confidence: 0.92,
|
||||
createdAt: '2024-10-23 09:30',
|
||||
updatedAt: '2024-10-23 10:00',
|
||||
dueDate: '2024-10-24 12:00',
|
||||
executedAt: '2024-10-23 10:00',
|
||||
executedBy: '李四',
|
||||
location: { lat: 39.8942, lng: 116.3974 },
|
||||
modelVersion: 'v2.0.5',
|
||||
ruleCount: 3,
|
||||
},
|
||||
{
|
||||
id: 'dec_004',
|
||||
type: 'soil',
|
||||
title: '4号地块土壤改良',
|
||||
description: 'pH值偏低,建议施用石灰调节土壤酸碱度',
|
||||
status: 'completed',
|
||||
priority: 'low',
|
||||
fieldId: 'field_004',
|
||||
fieldName: '4号地块',
|
||||
fieldArea: 4.2,
|
||||
cropType: '玉米',
|
||||
confidence: 0.85,
|
||||
createdAt: '2024-10-22 15:00',
|
||||
updatedAt: '2024-10-22 16:30',
|
||||
dueDate: '2024-10-25 12:00',
|
||||
executedAt: '2024-10-22 15:30',
|
||||
executedBy: '张三',
|
||||
completedAt: '2024-10-22 16:30',
|
||||
location: { lat: 39.8842, lng: 116.3874 },
|
||||
modelVersion: 'v1.8.2',
|
||||
ruleCount: 1,
|
||||
},
|
||||
{
|
||||
id: 'dec_005',
|
||||
type: 'weather',
|
||||
title: '5号大棚温度调控',
|
||||
description: '预计晚间温度降至12℃,建议提前加温',
|
||||
status: 'generated',
|
||||
priority: 'high',
|
||||
fieldId: 'field_005',
|
||||
fieldName: '5号大棚',
|
||||
fieldArea: 2.8,
|
||||
cropType: '黄瓜',
|
||||
confidence: 0.91,
|
||||
createdAt: '2024-10-23 16:00',
|
||||
updatedAt: '2024-10-23 16:00',
|
||||
dueDate: '2024-10-23 20:00',
|
||||
location: { lat: 39.9242, lng: 116.4274 },
|
||||
modelVersion: 'v2.3.1',
|
||||
ruleCount: 2,
|
||||
},
|
||||
{
|
||||
id: 'dec_006',
|
||||
type: 'harvest',
|
||||
title: '6号地块水稻收获',
|
||||
description: '水稻已达成熟期,建议3天内完成收获',
|
||||
status: 'generated',
|
||||
priority: 'urgent',
|
||||
fieldId: 'field_006',
|
||||
fieldName: '6号地块',
|
||||
fieldArea: 8.0,
|
||||
cropType: '水稻',
|
||||
confidence: 0.94,
|
||||
createdAt: '2024-10-23 08:00',
|
||||
updatedAt: '2024-10-23 08:00',
|
||||
dueDate: '2024-10-26 18:00',
|
||||
location: { lat: 39.9342, lng: 116.3874 },
|
||||
modelVersion: 'v2.5.0',
|
||||
ruleCount: 3,
|
||||
},
|
||||
{
|
||||
id: 'dec_007',
|
||||
type: 'irrigation',
|
||||
title: '7号大棚茄子补水',
|
||||
description: '连续3天无降雨,土壤湿度偏低',
|
||||
status: 'completed',
|
||||
priority: 'medium',
|
||||
fieldId: 'field_007',
|
||||
fieldName: '7号大棚',
|
||||
fieldArea: 2.2,
|
||||
cropType: '茄子',
|
||||
confidence: 0.86,
|
||||
createdAt: '2024-10-22 18:00',
|
||||
updatedAt: '2024-10-23 09:00',
|
||||
dueDate: '2024-10-23 12:00',
|
||||
executedAt: '2024-10-22 19:00',
|
||||
executedBy: '赵六',
|
||||
completedAt: '2024-10-23 09:00',
|
||||
location: { lat: 39.8742, lng: 116.4174 },
|
||||
modelVersion: 'v2.1.3',
|
||||
ruleCount: 2,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始趋势数据
|
||||
const initialTrendData: TrendData[] = [
|
||||
{ date: '10-17', generated: 8, completed: 5 },
|
||||
{ date: '10-18', generated: 12, completed: 8 },
|
||||
{ date: '10-19', generated: 10, completed: 9 },
|
||||
{ date: '10-20', generated: 15, completed: 11 },
|
||||
{ date: '10-21', generated: 13, completed: 10 },
|
||||
{ date: '10-22', generated: 11, completed: 9 },
|
||||
{ date: '10-23', generated: 14, completed: 7 },
|
||||
];
|
||||
|
||||
// 计算统计数据
|
||||
const calculateStats = (decisions: DecisionRecord[]): DecisionStats => {
|
||||
return {
|
||||
total: decisions.length,
|
||||
generated: decisions.filter(d => d.status === 'generated').length,
|
||||
executing: decisions.filter(d => d.status === 'executing').length,
|
||||
completed: decisions.filter(d => d.status === 'completed').length,
|
||||
urgent: decisions.filter(d => d.priority === 'urgent').length,
|
||||
avgConfidence: decisions.reduce((sum, d) => sum + d.confidence, 0) / decisions.length,
|
||||
};
|
||||
};
|
||||
|
||||
// 计算地块决策信息
|
||||
const calculateFieldDecisions = (decisions: DecisionRecord[]): FieldDecisionInfo[] => {
|
||||
const fieldDecisionMap = new Map<string, FieldDecisionInfo>();
|
||||
|
||||
decisions.forEach(decision => {
|
||||
if (!fieldDecisionMap.has(decision.fieldId)) {
|
||||
fieldDecisionMap.set(decision.fieldId, {
|
||||
fieldId: decision.fieldId,
|
||||
fieldName: decision.fieldName,
|
||||
location: decision.location,
|
||||
area: decision.fieldArea,
|
||||
cropType: decision.cropType,
|
||||
decisions: [],
|
||||
urgentCount: 0,
|
||||
generatedCount: 0,
|
||||
executingCount: 0,
|
||||
completedCount: 0,
|
||||
});
|
||||
}
|
||||
const fieldInfo = fieldDecisionMap.get(decision.fieldId)!;
|
||||
fieldInfo.decisions.push(decision);
|
||||
if (decision.priority === 'urgent') fieldInfo.urgentCount++;
|
||||
if (decision.status === 'generated') fieldInfo.generatedCount++;
|
||||
if (decision.status === 'executing') fieldInfo.executingCount++;
|
||||
if (decision.status === 'completed') fieldInfo.completedCount++;
|
||||
});
|
||||
|
||||
return Array.from(fieldDecisionMap.values());
|
||||
};
|
||||
|
||||
// 计算最新决策
|
||||
const calculateLatestDecisions = (decisions: DecisionRecord[]): DecisionRecord[] => {
|
||||
return [...decisions]
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.slice(0, 5);
|
||||
};
|
||||
|
||||
// 保存到本地存储
|
||||
const saveToStorage = (state: AIDecisionDashboardState) => {
|
||||
try {
|
||||
localStorage.setItem('ai-decision-dashboard', JSON.stringify({
|
||||
decisions: state.decisions,
|
||||
lastUpdated: state.lastUpdated,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从本地存储加载
|
||||
const loadFromStorage = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem('ai-decision-dashboard');
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored);
|
||||
return {
|
||||
decisions: data.decisions || initialDecisions,
|
||||
lastUpdated: data.lastUpdated || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load from localStorage:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 计算派生状态
|
||||
const calculateDerivedState = (decisions: DecisionRecord[]) => {
|
||||
const stats = calculateStats(decisions);
|
||||
const fieldDecisions = calculateFieldDecisions(decisions);
|
||||
const latestDecisions = calculateLatestDecisions(decisions);
|
||||
|
||||
return {
|
||||
stats,
|
||||
fieldDecisions,
|
||||
latestDecisions,
|
||||
trendData: initialTrendData,
|
||||
};
|
||||
};
|
||||
|
||||
// 初始状态
|
||||
export const initialState: AIDecisionDashboardState = (() => {
|
||||
const stored = loadFromStorage();
|
||||
const decisions = stored?.decisions || initialDecisions;
|
||||
const derivedState = calculateDerivedState(decisions);
|
||||
|
||||
return {
|
||||
decisions,
|
||||
...derivedState,
|
||||
lastUpdated: stored?.lastUpdated || new Date().toISOString(),
|
||||
};
|
||||
})();
|
||||
|
||||
// Reducer
|
||||
export function AIDecisionDashboardReducer(
|
||||
state: AIDecisionDashboardState,
|
||||
action: AIDecisionDashboardAction
|
||||
): AIDecisionDashboardState {
|
||||
switch (action.type) {
|
||||
case 'SET_DECISIONS': {
|
||||
const derivedState = calculateDerivedState(action.payload);
|
||||
const newState = {
|
||||
...state,
|
||||
decisions: action.payload,
|
||||
...derivedState,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
saveToStorage(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'ADD_DECISION': {
|
||||
const newDecisions = [...state.decisions, action.payload];
|
||||
const derivedState = calculateDerivedState(newDecisions);
|
||||
const newState = {
|
||||
...state,
|
||||
decisions: newDecisions,
|
||||
...derivedState,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
saveToStorage(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'UPDATE_DECISION': {
|
||||
const newDecisions = state.decisions.map(decision =>
|
||||
decision.id === action.payload.id
|
||||
? { ...decision, ...action.payload.updates }
|
||||
: decision
|
||||
);
|
||||
const derivedState = calculateDerivedState(newDecisions);
|
||||
const newState = {
|
||||
...state,
|
||||
decisions: newDecisions,
|
||||
...derivedState,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
saveToStorage(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'DELETE_DECISION': {
|
||||
const newDecisions = state.decisions.filter(decision => decision.id !== action.payload);
|
||||
const derivedState = calculateDerivedState(newDecisions);
|
||||
const newState = {
|
||||
...state,
|
||||
decisions: newDecisions,
|
||||
...derivedState,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
saveToStorage(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'REFRESH_DATA': {
|
||||
const derivedState = calculateDerivedState(state.decisions);
|
||||
const newState = {
|
||||
...state,
|
||||
...derivedState,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
saveToStorage(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'LOAD_FROM_STORAGE': {
|
||||
const decisions = action.payload.decisions || state.decisions;
|
||||
const derivedState = calculateDerivedState(decisions);
|
||||
return {
|
||||
...state,
|
||||
decisions,
|
||||
...derivedState,
|
||||
lastUpdated: action.payload.lastUpdated || state.lastUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,104 @@
|
||||
/**
|
||||
* filekorolheader: AI决策看板 - 智能决策生成与执行监控中心
|
||||
* 功能:决策统计展示、地图可视化、趋势分析、决策详情查看、状态管理
|
||||
* 路径:/ai-crop-model/support/dashboard
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useReducer } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
TrendingUp,
|
||||
Droplets,
|
||||
Bug,
|
||||
Sprout,
|
||||
Package,
|
||||
CloudRain,
|
||||
Layers,
|
||||
Activity,
|
||||
Clock,
|
||||
Info,
|
||||
Sparkles,
|
||||
Calendar,
|
||||
MapPin,
|
||||
Zap,
|
||||
CircleDot,
|
||||
Map as MapIcon,
|
||||
} from 'lucide-react';
|
||||
import { AIDecisionDashboardReducer, initialState, AIDecisionDashboardState, AIDecisionDashboardAction } from './components/aiDecisionDashboardReducer';
|
||||
import { StatisticsCards } from './components/StatisticsCards';
|
||||
import { DecisionMap } from './components/DecisionMap';
|
||||
import { DecisionTrends } from './components/DecisionTrends';
|
||||
import { LatestDecisions } from './components/LatestDecisions';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [state, dispatch] = useReducer(AIDecisionDashboardReducer, initialState);
|
||||
|
||||
const handleRefresh = () => {
|
||||
dispatch({ type: 'REFRESH_DATA' });
|
||||
toast.success('数据已刷新');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold">决策仪表盘</h2>
|
||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
||||
<p className="text-sm">
|
||||
<strong>页面路径:</strong> /ai-crop-model/support/dashboard
|
||||
{/* 页面标题 */}
|
||||
<Card className="p-6 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950 dark:to-emerald-950 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<LayoutDashboard className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h2 className="mb-2">AI决策看板</h2>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
实时展示AI决策生成与执行统计,监控决策效果和趋势
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className="bg-white dark:bg-gray-800">
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
智能生成
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-white dark:bg-gray-800">
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
实时监控
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-white dark:bg-gray-800">
|
||||
<TrendingUp className="w-3 h-3 mr-1" />
|
||||
趋势分析
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-white dark:bg-gray-800">
|
||||
<MapIcon className="w-3 h-3 mr-1" />
|
||||
地图可视化
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 核心统计卡片 */}
|
||||
<StatisticsCards stats={state.stats} />
|
||||
|
||||
{/* 地图可视化 + 决策趋势图 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<DecisionMap fieldDecisions={state.fieldDecisions} />
|
||||
<DecisionTrends trendData={state.trendData} />
|
||||
</div>
|
||||
|
||||
{/* 最新决策建议 */}
|
||||
<LatestDecisions latestDecisions={state.latestDecisions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user