Files
smart-cropx-ui/src/app/(app)/ai-crop-model/support/dashboard/components/DecisionMap.tsx
2025-11-10 09:19:56 +08:00

187 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
);
}