子仓库提交

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,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>
);
}