生产管理系统前端 - 地块对比分析,地块适宜性评价开发
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Plus, X } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||||
|
|
||||||
|
interface FieldSelectorProps {
|
||||||
|
fields: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldSelector({ fields }: FieldSelectorProps) {
|
||||||
|
const { state, addField, removeField } = useChartAnalysis();
|
||||||
|
|
||||||
|
const availableFields = fields.filter(f => !state.selectedFields.includes(f.id));
|
||||||
|
const comparisonFields = fields.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
const handleAddField = (fieldId: string) => {
|
||||||
|
const success = addField(fieldId);
|
||||||
|
if (!success) {
|
||||||
|
toast.error('最多只能同时对比4个地块');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveField = (fieldId: string) => {
|
||||||
|
const success = removeField(fieldId);
|
||||||
|
if (!success) {
|
||||||
|
toast.error('至少需要选择2个地块进行对比');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">选择对比地块 (2-4个)</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<Badge
|
||||||
|
key={field.id}
|
||||||
|
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-3 py-1.5 flex items-center gap-2 font-light"
|
||||||
|
>
|
||||||
|
{field.name}
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveField(field.id)}
|
||||||
|
className="hover:bg-green-200 dark:hover:bg-green-800 rounded-full p-0.5"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{availableFields.length > 0 && (
|
||||||
|
<Select onValueChange={handleAddField}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>添加地块</span>
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableFields.map(field => (
|
||||||
|
<SelectItem key={field.id} value={field.id}>
|
||||||
|
{field.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Map as MapIcon, MapPin } from 'lucide-react';
|
||||||
|
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||||
|
|
||||||
|
export function MapComparison() {
|
||||||
|
const { state } = useChartAnalysis();
|
||||||
|
|
||||||
|
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return 'bg-green-500 text-white';
|
||||||
|
case '一般适宜': return 'bg-yellow-500 text-white';
|
||||||
|
case '不适宜': return 'bg-red-500 text-white';
|
||||||
|
default: return 'bg-gray-500 text-white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果没有选择地块,显示空状态
|
||||||
|
if (comparisonFields.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<MapIcon className="w-5 h-5 text-blue-600" />
|
||||||
|
地块空间分布对比
|
||||||
|
</h3>
|
||||||
|
<div className="h-64 flex items-center justify-center">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<MapIcon className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>请先选择要对比的地块</p>
|
||||||
|
<p className="text-sm mt-2">地图将显示各地块的空间分布位置</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<MapIcon className="w-5 h-5 text-blue-600" />
|
||||||
|
地块空间分布对比
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{comparisonFields.map((field, index) => (
|
||||||
|
<div key={field.id} className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">{field.name}</h4>
|
||||||
|
<div className="h-48 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900 dark:to-green-800 rounded-lg flex items-center justify-center relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 opacity-20">
|
||||||
|
<div className="grid grid-cols-8 grid-rows-8 h-full w-full">
|
||||||
|
{Array.from({ length: 64 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="border border-green-300 dark:border-green-600"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `rgba(34, 197, 94, ${0.1 + (field.suitabilityScore / 100) * 0.6})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative text-center">
|
||||||
|
<MapPin className="w-12 h-12 text-green-600 dark:text-green-400 mx-auto mb-2" />
|
||||||
|
<p className="text-sm font-medium">{field.location}</p>
|
||||||
|
<Badge className={`mt-2 ${getGradeColor(field.suitabilityGrade)}`}>
|
||||||
|
{field.suitabilityGrade}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
ResponsiveContainer,
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
} from 'recharts';
|
||||||
|
import { Scale } from 'lucide-react';
|
||||||
|
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||||
|
|
||||||
|
export function NutrientComparison() {
|
||||||
|
const { state } = useChartAnalysis();
|
||||||
|
|
||||||
|
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
// 养分对比数据
|
||||||
|
const nutrientData = comparisonFields.map(field => ({
|
||||||
|
name: field.name,
|
||||||
|
全氮: field.nitrogen,
|
||||||
|
全磷: field.phosphorus,
|
||||||
|
全钾: field.potassium,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 如果没有选择地块,显示空状态
|
||||||
|
if (comparisonFields.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<Scale className="w-5 h-5 text-orange-600" />
|
||||||
|
土壤养分对比 (g/kg)
|
||||||
|
</h3>
|
||||||
|
<div className="h-80 flex items-center justify-center">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<Scale className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-sm">请选择地块</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<Scale className="w-5 h-5 text-orange-600" />
|
||||||
|
土壤养分对比 (g/kg)
|
||||||
|
</h3>
|
||||||
|
<div className="h-80">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<LineChart data={nutrientData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line type="monotone" dataKey="全氮" stroke="#10b981" strokeWidth={2} />
|
||||||
|
<Line type="monotone" dataKey="全磷" stroke="#3b82f6" strokeWidth={2} />
|
||||||
|
<Line type="monotone" dataKey="全钾" stroke="#f59e0b" strokeWidth={2} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
RadarChart,
|
||||||
|
Radar as RechartsRadar,
|
||||||
|
PolarGrid,
|
||||||
|
PolarAngleAxis,
|
||||||
|
PolarRadiusAxis,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Legend,
|
||||||
|
} from 'recharts';
|
||||||
|
import { Radar } from 'lucide-react';
|
||||||
|
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||||
|
|
||||||
|
export function ChartRadarAnalysis() {
|
||||||
|
const { state } = useChartAnalysis();
|
||||||
|
|
||||||
|
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
// 雷达图数据
|
||||||
|
const radarData = [
|
||||||
|
{
|
||||||
|
indicator: 'pH值',
|
||||||
|
...comparisonFields.reduce((acc, field) => ({
|
||||||
|
...acc,
|
||||||
|
[field.name]: (field.ph / 10) * 100,
|
||||||
|
}), {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indicator: '有机质',
|
||||||
|
...comparisonFields.reduce((acc, field) => ({
|
||||||
|
...acc,
|
||||||
|
[field.name]: (field.organicMatter / 40) * 100,
|
||||||
|
}), {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indicator: '全氮',
|
||||||
|
...comparisonFields.reduce((acc, field) => ({
|
||||||
|
...acc,
|
||||||
|
[field.name]: (field.nitrogen / 2.5) * 100,
|
||||||
|
}), {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indicator: '全磷',
|
||||||
|
...comparisonFields.reduce((acc, field) => ({
|
||||||
|
...acc,
|
||||||
|
[field.name]: (field.phosphorus / 2.0) * 100,
|
||||||
|
}), {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indicator: '全钾',
|
||||||
|
...comparisonFields.reduce((acc, field) => ({
|
||||||
|
...acc,
|
||||||
|
[field.name]: (field.potassium / 25) * 100,
|
||||||
|
}), {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indicator: '土层厚度',
|
||||||
|
...comparisonFields.reduce((acc, field) => ({
|
||||||
|
...acc,
|
||||||
|
[field.name]: (field.soilDepth / 100) * 100,
|
||||||
|
}), {}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const colors = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'];
|
||||||
|
|
||||||
|
// 如果没有选择地块,显示空状态
|
||||||
|
if (comparisonFields.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<Radar className="w-5 h-5 text-blue-600" />
|
||||||
|
多维度雷达图对比
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
综合展示各地块在多个指标上的相对优劣势
|
||||||
|
</p>
|
||||||
|
<div className="h-96 flex items-center justify-center">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<Radar className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>请先选择要对比的地块</p>
|
||||||
|
<p className="text-sm mt-2">雷达图将显示各地块的多维度指标对比</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<Radar className="w-5 h-5 text-blue-600" />
|
||||||
|
多维度雷达图对比
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
综合展示各地块在多个指标上的相对优劣势
|
||||||
|
</p>
|
||||||
|
<div className="h-96">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<RadarChart data={radarData}>
|
||||||
|
<PolarGrid />
|
||||||
|
<PolarAngleAxis dataKey="indicator" />
|
||||||
|
<PolarRadiusAxis angle={90} domain={[0, 100]} />
|
||||||
|
{comparisonFields.map((field, index) => (
|
||||||
|
<RechartsRadar
|
||||||
|
key={field.id}
|
||||||
|
name={field.name}
|
||||||
|
dataKey={field.name}
|
||||||
|
stroke={colors[index]}
|
||||||
|
fill={colors[index]}
|
||||||
|
fillOpacity={0.3}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Legend />
|
||||||
|
</RadarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
ResponsiveContainer,
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
} from 'recharts';
|
||||||
|
import { BarChart3, TrendingUp } from 'lucide-react';
|
||||||
|
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||||
|
|
||||||
|
export function YieldComparison() {
|
||||||
|
const { state } = useChartAnalysis();
|
||||||
|
|
||||||
|
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
// 产量对比数据
|
||||||
|
const yieldData = comparisonFields.map(field => ({
|
||||||
|
name: field.name,
|
||||||
|
产量: field.yield,
|
||||||
|
有机质: field.organicMatter,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 如果没有选择地块,显示空状态
|
||||||
|
if (comparisonFields.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-5 h-5 text-green-600" />
|
||||||
|
产量对比 (kg/亩)
|
||||||
|
</h3>
|
||||||
|
<div className="h-72 flex items-center justify-center">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<BarChart3 className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-sm">请选择地块</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||||||
|
有机质含量对比 (g/kg)
|
||||||
|
</h3>
|
||||||
|
<div className="h-72 flex items-center justify-center">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<TrendingUp className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-sm">请选择地块</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-5 h-5 text-green-600" />
|
||||||
|
产量对比 (kg/亩)
|
||||||
|
</h3>
|
||||||
|
<div className="h-72">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={yieldData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="产量" fill="#10b981" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||||||
|
有机质含量对比 (g/kg)
|
||||||
|
</h3>
|
||||||
|
<div className="h-72">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={yieldData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="有机质" fill="#8b5cf6" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface FieldData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
area: number;
|
||||||
|
location: string;
|
||||||
|
soilType: string;
|
||||||
|
ph: number;
|
||||||
|
organicMatter: number;
|
||||||
|
nitrogen: number;
|
||||||
|
phosphorus: number;
|
||||||
|
potassium: number;
|
||||||
|
soilDepth: number;
|
||||||
|
slope: number;
|
||||||
|
currentCrop: string;
|
||||||
|
yield: number;
|
||||||
|
suitabilityScore: number;
|
||||||
|
suitabilityGrade: '高度适宜' | '一般适宜' | '不适宜';
|
||||||
|
irrigation: string;
|
||||||
|
drainage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartAnalysisState {
|
||||||
|
selectedFields: string[];
|
||||||
|
fields: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChartAnalysisAction =
|
||||||
|
| { type: 'SET_FIELDS'; payload: FieldData[] }
|
||||||
|
| { type: 'ADD_FIELD'; payload: string }
|
||||||
|
| { type: 'REMOVE_FIELD'; payload: string }
|
||||||
|
| { type: 'SET_SELECTED_FIELDS'; payload: string[] };
|
||||||
|
|
||||||
|
const initialState: ChartAnalysisState = {
|
||||||
|
selectedFields: ['field-1', 'field-2'],
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function chartAnalysisReducer(state: ChartAnalysisState, action: ChartAnalysisAction): ChartAnalysisState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_FIELDS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fields: action.payload,
|
||||||
|
};
|
||||||
|
case 'ADD_FIELD':
|
||||||
|
if (state.selectedFields.length >= 4) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedFields: [...state.selectedFields, action.payload],
|
||||||
|
};
|
||||||
|
case 'REMOVE_FIELD':
|
||||||
|
if (state.selectedFields.length <= 2) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedFields: state.selectedFields.filter(id => id !== action.payload),
|
||||||
|
};
|
||||||
|
case 'SET_SELECTED_FIELDS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedFields: action.payload,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChartAnalysis() {
|
||||||
|
const [state, dispatch] = useReducer(chartAnalysisReducer, initialState);
|
||||||
|
|
||||||
|
const addField = useCallback((fieldId: string) => {
|
||||||
|
if (state.selectedFields.length >= 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dispatch({ type: 'ADD_FIELD', payload: fieldId });
|
||||||
|
return true;
|
||||||
|
}, [state.selectedFields.length]);
|
||||||
|
|
||||||
|
const removeField = useCallback((fieldId: string) => {
|
||||||
|
if (state.selectedFields.length <= 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dispatch({ type: 'REMOVE_FIELD', payload: fieldId });
|
||||||
|
return true;
|
||||||
|
}, [state.selectedFields.length]);
|
||||||
|
|
||||||
|
const setSelectedFields = useCallback((fieldIds: string[]) => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_FIELDS', payload: fieldIds });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setFields = useCallback((fields: FieldData[]) => {
|
||||||
|
dispatch({ type: 'SET_FIELDS', payload: fields });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
dispatch,
|
||||||
|
addField,
|
||||||
|
removeField,
|
||||||
|
setSelectedFields,
|
||||||
|
setFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,18 +1,158 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { FieldSelector } from './components/FieldSelector';
|
||||||
|
import { ChartRadarAnalysis } from './components/RadarChart';
|
||||||
|
import { YieldComparison } from './components/YieldComparison';
|
||||||
|
import { NutrientComparison } from './components/NutrientComparison';
|
||||||
|
import { MapComparison } from './components/MapComparison';
|
||||||
|
import { useChartAnalysis, FieldData } from './components/chartAnalysisReducer';
|
||||||
|
|
||||||
|
// 模拟地块数据
|
||||||
|
const mockFieldsData: FieldData[] = [
|
||||||
|
{
|
||||||
|
id: 'field-1',
|
||||||
|
name: '东区1号地',
|
||||||
|
area: 50.5,
|
||||||
|
location: '东经120.15°, 北纬30.25°',
|
||||||
|
soilType: '壤土',
|
||||||
|
ph: 6.5,
|
||||||
|
organicMatter: 32,
|
||||||
|
nitrogen: 1.8,
|
||||||
|
phosphorus: 1.2,
|
||||||
|
potassium: 18,
|
||||||
|
soilDepth: 85,
|
||||||
|
slope: 3,
|
||||||
|
currentCrop: '水稻',
|
||||||
|
yield: 750,
|
||||||
|
suitabilityScore: 87,
|
||||||
|
suitabilityGrade: '高度适宜',
|
||||||
|
irrigation: '喷灌',
|
||||||
|
drainage: '良好',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-2',
|
||||||
|
name: '西区2号地',
|
||||||
|
area: 45.2,
|
||||||
|
location: '东经120.12°, 北纬30.28°',
|
||||||
|
soilType: '粘土',
|
||||||
|
ph: 7.8,
|
||||||
|
organicMatter: 22,
|
||||||
|
nitrogen: 1.3,
|
||||||
|
phosphorus: 0.9,
|
||||||
|
potassium: 14,
|
||||||
|
soilDepth: 55,
|
||||||
|
slope: 5,
|
||||||
|
currentCrop: '玉米',
|
||||||
|
yield: 650,
|
||||||
|
suitabilityScore: 72,
|
||||||
|
suitabilityGrade: '一般适宜',
|
||||||
|
irrigation: '滴灌',
|
||||||
|
drainage: '中等',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-3',
|
||||||
|
name: '南区3号地',
|
||||||
|
area: 38.8,
|
||||||
|
location: '东经120.18°, 北纬30.22°',
|
||||||
|
soilType: '砂土',
|
||||||
|
ph: 8.5,
|
||||||
|
organicMatter: 15,
|
||||||
|
nitrogen: 0.8,
|
||||||
|
phosphorus: 0.6,
|
||||||
|
potassium: 10,
|
||||||
|
soilDepth: 42,
|
||||||
|
slope: 8,
|
||||||
|
currentCrop: '小麦',
|
||||||
|
yield: 480,
|
||||||
|
suitabilityScore: 58,
|
||||||
|
suitabilityGrade: '不适宜',
|
||||||
|
irrigation: '漫灌',
|
||||||
|
drainage: '较差',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-4',
|
||||||
|
name: '北区4号地',
|
||||||
|
area: 55.0,
|
||||||
|
location: '东经120.20°, 北纬30.30°',
|
||||||
|
soilType: '壤土',
|
||||||
|
ph: 6.8,
|
||||||
|
organicMatter: 28,
|
||||||
|
nitrogen: 1.6,
|
||||||
|
phosphorus: 1.0,
|
||||||
|
potassium: 16,
|
||||||
|
soilDepth: 75,
|
||||||
|
slope: 2,
|
||||||
|
currentCrop: '大豆',
|
||||||
|
yield: 380,
|
||||||
|
suitabilityScore: 82,
|
||||||
|
suitabilityGrade: '高度适宜',
|
||||||
|
irrigation: '喷灌',
|
||||||
|
drainage: '良好',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function ChartPage() {
|
export default function ChartPage() {
|
||||||
|
const { setFields, state } = useChartAnalysis();
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
useEffect(() => {
|
||||||
|
// 直接使用模拟数据,确保数据可用
|
||||||
|
setFields(mockFieldsData);
|
||||||
|
localStorage.setItem('chart-analysis-fields', JSON.stringify(mockFieldsData));
|
||||||
|
}, [setFields]);
|
||||||
|
|
||||||
|
// 如果数据还没有加载,显示loading状态
|
||||||
|
if (state.fields.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold">图表对比</h2>
|
<div>
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
<h2 className="text-green-800 dark:text-green-200">可视化图表分析</h2>
|
||||||
<p className="text-sm">
|
<p className="text-muted-foreground">
|
||||||
<strong>页面路径:</strong> /land-information/comparison/chart
|
多维度指标可视化展示与对比分析
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p>正在加载数据...</p>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-green-800 dark:text-green-200">可视化图表分析</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
多维度指标可视化展示与对比分析
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地块选择器 */}
|
||||||
|
<FieldSelector fields={state.fields} />
|
||||||
|
|
||||||
|
{/* 图表分析区域 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 雷达图 */}
|
||||||
|
<ChartRadarAnalysis />
|
||||||
|
|
||||||
|
{/* 产量与有机质对比 */}
|
||||||
|
<YieldComparison />
|
||||||
|
|
||||||
|
{/* 养分对比 */}
|
||||||
|
<NutrientComparison />
|
||||||
|
|
||||||
|
{/* 地图对比 */}
|
||||||
|
<MapComparison />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { MapPin } from 'lucide-react';
|
||||||
|
import { FieldData } from './multiDimensionReducer';
|
||||||
|
|
||||||
|
interface BasicPropertiesProps {
|
||||||
|
comparisonFields: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BasicProperties({ comparisonFields }: BasicPropertiesProps) {
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<MapPin className="w-5 h-5 text-blue-600" />
|
||||||
|
<h3>基础属性对比</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs">指标项</th>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<th key={field.id} className="px-4 py-3 text-center text-xs">
|
||||||
|
{field.name}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">面积 (亩)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
{field.area}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">位置</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
{field.location}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">土壤类型</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<Badge variant="outline">{field.soilType}</Badge>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">坡度 (°)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<span className={field.slope <= 3 ? 'text-green-600' : field.slope <= 5 ? 'text-yellow-600' : 'text-red-600'}>
|
||||||
|
{field.slope}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Plus, X } from 'lucide-react';
|
||||||
|
import { FieldData } from './multiDimensionReducer';
|
||||||
|
|
||||||
|
interface FieldSelectorProps {
|
||||||
|
selectedFields: string[];
|
||||||
|
fieldsData: FieldData[];
|
||||||
|
onAddField: (fieldId: string) => void;
|
||||||
|
onRemoveField: (fieldId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldSelector({
|
||||||
|
selectedFields,
|
||||||
|
fieldsData,
|
||||||
|
onAddField,
|
||||||
|
onRemoveField
|
||||||
|
}: FieldSelectorProps) {
|
||||||
|
const comparisonFields = fieldsData.filter(f => selectedFields.includes(f.id));
|
||||||
|
const availableFields = fieldsData.filter(f => !selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return 'bg-green-500 text-white';
|
||||||
|
case '一般适宜': return 'bg-yellow-500 text-white';
|
||||||
|
case '不适宜': return 'bg-red-500 text-white';
|
||||||
|
default: return 'bg-gray-500 text-white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">选择对比地块 (2-4个)</label>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<Badge
|
||||||
|
key={field.id}
|
||||||
|
className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-3 py-1.5 flex items-center gap-2 border border-green-200 dark:border-green-800"
|
||||||
|
>
|
||||||
|
{field.name}
|
||||||
|
<button
|
||||||
|
onClick={() => onRemoveField(field.id)}
|
||||||
|
className="hover:bg-green-200 dark:hover:bg-green-800 rounded-full p-0.5 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{availableFields.length > 0 && (
|
||||||
|
<Select onValueChange={onAddField}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>添加地块</span>
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableFields.map(field => (
|
||||||
|
<SelectItem key={field.id} value={field.id}>
|
||||||
|
{field.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedFields.length < 2 && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
至少需要选择2个地块进行对比
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{selectedFields.length >= 4 && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
最多只能同时对比4个地块
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 选中地块的简要信息 */}
|
||||||
|
{comparisonFields.length > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">已选地块信息:</p>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<div key={field.id} className="p-3 bg-muted rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-xs font-medium truncate">{field.name}</span>
|
||||||
|
<Badge className={`${getGradeColor(field.suitabilityGrade)} text-xs`}>
|
||||||
|
{field.suitabilityGrade}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>面积:</span>
|
||||||
|
<span>{field.area}亩</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>作物:</span>
|
||||||
|
<span>{field.currentCrop}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>产量:</span>
|
||||||
|
<span>{field.yield}kg/亩</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>评分:</span>
|
||||||
|
<span>{field.suitabilityScore}分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { BarChart3 } from 'lucide-react';
|
||||||
|
import { FieldData } from './multiDimensionReducer';
|
||||||
|
|
||||||
|
interface ManagementStatusProps {
|
||||||
|
comparisonFields: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ManagementStatus({ comparisonFields }: ManagementStatusProps) {
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return 'bg-green-500 text-white';
|
||||||
|
case '一般适宜': return 'bg-yellow-500 text-white';
|
||||||
|
case '不适宜': return 'bg-red-500 text-white';
|
||||||
|
default: return 'bg-gray-500 text-white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getYieldColor = (yieldValue: number) => {
|
||||||
|
if (yieldValue >= 700) return 'text-green-600';
|
||||||
|
if (yieldValue >= 500) return 'text-yellow-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<BarChart3 className="w-5 h-5 text-purple-600" />
|
||||||
|
<h3>经营现状对比</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs">指标项</th>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<th key={field.id} className="px-4 py-3 text-center text-xs">
|
||||||
|
{field.name}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">当前作物</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<Badge variant="outline">{field.currentCrop}</Badge>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">产量 (kg/亩)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center">
|
||||||
|
<span className={`text-sm font-medium ${getYieldColor(field.yield)}`}>
|
||||||
|
{field.yield}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
{field.yield >= 700 ? '高产' : field.yield >= 500 ? '中产' : '低产'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">灌溉方式</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<Badge
|
||||||
|
variant={field.irrigation === '喷灌' ? 'default' :
|
||||||
|
field.irrigation === '滴灌' ? 'secondary' : 'outline'}
|
||||||
|
>
|
||||||
|
{field.irrigation}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">排水状况</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<Badge
|
||||||
|
variant={field.drainage === '良好' ? 'default' :
|
||||||
|
field.drainage === '中等' ? 'secondary' : 'destructive'}
|
||||||
|
>
|
||||||
|
{field.drainage}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Sprout } from 'lucide-react';
|
||||||
|
import { CheckCircle2, AlertCircle } from 'lucide-react';
|
||||||
|
import { FieldData } from './multiDimensionReducer';
|
||||||
|
|
||||||
|
interface NaturalConditionsProps {
|
||||||
|
comparisonFields: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NaturalConditions({ comparisonFields }: NaturalConditionsProps) {
|
||||||
|
const getPhStatus = (ph: number) => {
|
||||||
|
if (ph >= 6.0 && ph <= 7.5) return 'optimal';
|
||||||
|
return 'deviation';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrganicStatus = (organic: number) => {
|
||||||
|
if (organic >= 25) return 'optimal';
|
||||||
|
return 'deviation';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDepthStatus = (depth: number) => {
|
||||||
|
if (depth >= 60) return 'optimal';
|
||||||
|
return 'deviation';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Sprout className="w-5 h-5 text-green-600" />
|
||||||
|
<h3>自然条件对比</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs">指标项</th>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<th key={field.id} className="px-4 py-3 text-center text-xs">
|
||||||
|
{field.name}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">pH值</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<span className="text-sm">{field.ph}</span>
|
||||||
|
{getPhStatus(field.ph) === 'optimal' ? (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
最佳: 6.0-7.5
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">有机质 (g/kg)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<span className="text-sm">{field.organicMatter}</span>
|
||||||
|
{getOrganicStatus(field.organicMatter) === 'optimal' ? (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
最佳: ≥25
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">全氮 (g/kg)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<span className={field.nitrogen >= 1.5 ? 'text-green-600' : field.nitrogen >= 1.0 ? 'text-yellow-600' : 'text-red-600'}>
|
||||||
|
{field.nitrogen}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
最佳: ≥1.5
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">全磷 (g/kg)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<span className={field.phosphorus >= 1.0 ? 'text-green-600' : field.phosphorus >= 0.6 ? 'text-yellow-600' : 'text-red-600'}>
|
||||||
|
{field.phosphorus}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
最佳: ≥1.0
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">全钾 (g/kg)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<span className={field.potassium >= 15 ? 'text-green-600' : field.potassium >= 10 ? 'text-yellow-600' : 'text-red-600'}>
|
||||||
|
{field.potassium}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
最佳: ≥15
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">土层厚度 (cm)</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<span className="text-sm">{field.soilDepth}</span>
|
||||||
|
{getDepthStatus(field.soilDepth) === 'optimal' ? (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
最佳: ≥60
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Target } from 'lucide-react';
|
||||||
|
import { FieldData } from './multiDimensionReducer';
|
||||||
|
|
||||||
|
interface SuitabilityEvaluationProps {
|
||||||
|
comparisonFields: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SuitabilityEvaluation({ comparisonFields }: SuitabilityEvaluationProps) {
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return 'bg-green-500 text-white';
|
||||||
|
case '一般适宜': return 'bg-yellow-500 text-white';
|
||||||
|
case '不适宜': return 'bg-red-500 text-white';
|
||||||
|
default: return 'bg-gray-500 text-white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScoreColor = (score: number) => {
|
||||||
|
if (score >= 80) return 'text-green-600 dark:text-green-400';
|
||||||
|
if (score >= 60) return 'text-yellow-600 dark:text-yellow-400';
|
||||||
|
return 'text-red-600 dark:text-red-400';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScoreLevel = (score: number) => {
|
||||||
|
if (score >= 80) return '优秀';
|
||||||
|
if (score >= 60) return '良好';
|
||||||
|
return '需改善';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Target className="w-5 h-5 text-orange-600" />
|
||||||
|
<h3>适宜性评价对比</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs">指标项</th>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<th key={field.id} className="px-4 py-3 text-center text-xs">
|
||||||
|
{field.name}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">综合评分</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className={`text-2xl font-bold ${getScoreColor(field.suitabilityScore)}`}>
|
||||||
|
{field.suitabilityScore}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{getScoreLevel(field.suitabilityScore)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">适宜性等级</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Badge className={`${getGradeColor(field.suitabilityGrade)} text-sm px-3 py-1`}>
|
||||||
|
{field.suitabilityGrade}
|
||||||
|
</Badge>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{field.suitabilityGrade === '高度适宜' ? '适合多种作物' :
|
||||||
|
field.suitabilityGrade === '一般适宜' ? '适合耐性作物' : '需系统改良'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t border-border">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">主要限制因子</td>
|
||||||
|
{comparisonFields.map(field => {
|
||||||
|
const limitations = [];
|
||||||
|
if (field.ph < 6.0 || field.ph > 7.5) limitations.push('pH值');
|
||||||
|
if (field.organicMatter < 25) limitations.push('有机质');
|
||||||
|
if (field.soilDepth < 60) limitations.push('土层厚度');
|
||||||
|
if (field.nitrogen < 1.5) limitations.push('全氮');
|
||||||
|
if (field.phosphorus < 1.0) limitations.push('全磷');
|
||||||
|
if (field.potassium < 15) limitations.push('全钾');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
{limitations.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{limitations.slice(0, 2).map((limit, index) => (
|
||||||
|
<Badge key={index} variant="outline" className="text-xs">
|
||||||
|
{limit}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{limitations.length > 2 && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
+{limitations.length - 2}个
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-green-600">无限制因子</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm font-medium">推荐作物类型</td>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<td key={field.id} className="px-4 py-3 text-center text-sm">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{field.suitabilityGrade === '高度适宜' ? (
|
||||||
|
<>
|
||||||
|
<Badge className="bg-green-100 text-green-800 border-green-200" variant="outline">
|
||||||
|
经济作物
|
||||||
|
</Badge>
|
||||||
|
<Badge className="bg-blue-100 text-blue-800 border-blue-200" variant="outline">
|
||||||
|
粮食作物
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
) : field.suitabilityGrade === '一般适宜' ? (
|
||||||
|
<Badge className="bg-yellow-100 text-yellow-800 border-yellow-200" variant="outline">
|
||||||
|
耐性作物
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge className="bg-red-100 text-red-800 border-red-200" variant="outline">
|
||||||
|
需改良后种植
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
export interface FieldData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
area: number;
|
||||||
|
location: string;
|
||||||
|
soilType: string;
|
||||||
|
ph: number;
|
||||||
|
organicMatter: number;
|
||||||
|
nitrogen: number;
|
||||||
|
phosphorus: number;
|
||||||
|
potassium: number;
|
||||||
|
soilDepth: number;
|
||||||
|
slope: number;
|
||||||
|
currentCrop: string;
|
||||||
|
yield: number;
|
||||||
|
suitabilityScore: number;
|
||||||
|
suitabilityGrade: '高度适宜' | '一般适宜' | '不适宜';
|
||||||
|
irrigation: string;
|
||||||
|
drainage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultiDimensionState {
|
||||||
|
selectedFields: string[];
|
||||||
|
fieldsData: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MultiDimensionAction =
|
||||||
|
| { type: 'SET_SELECTED_FIELDS'; payload: string[] }
|
||||||
|
| { type: 'ADD_FIELD'; payload: string }
|
||||||
|
| { type: 'REMOVE_FIELD'; payload: string }
|
||||||
|
| { type: 'RESET_FIELDS' };
|
||||||
|
|
||||||
|
// 模拟地块数据
|
||||||
|
const initialFieldsData: FieldData[] = [
|
||||||
|
{
|
||||||
|
id: 'field-1',
|
||||||
|
name: '东区1号地',
|
||||||
|
area: 50.5,
|
||||||
|
location: '东经120.15°, 北纬30.25°',
|
||||||
|
soilType: '壤土',
|
||||||
|
ph: 6.5,
|
||||||
|
organicMatter: 32,
|
||||||
|
nitrogen: 1.8,
|
||||||
|
phosphorus: 1.2,
|
||||||
|
potassium: 18,
|
||||||
|
soilDepth: 85,
|
||||||
|
slope: 3,
|
||||||
|
currentCrop: '水稻',
|
||||||
|
yield: 750,
|
||||||
|
suitabilityScore: 87,
|
||||||
|
suitabilityGrade: '高度适宜',
|
||||||
|
irrigation: '喷灌',
|
||||||
|
drainage: '良好',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-2',
|
||||||
|
name: '西区2号地',
|
||||||
|
area: 45.2,
|
||||||
|
location: '东经120.12°, 北纬30.28°',
|
||||||
|
soilType: '粘土',
|
||||||
|
ph: 7.8,
|
||||||
|
organicMatter: 22,
|
||||||
|
nitrogen: 1.3,
|
||||||
|
phosphorus: 0.9,
|
||||||
|
potassium: 14,
|
||||||
|
soilDepth: 55,
|
||||||
|
slope: 5,
|
||||||
|
currentCrop: '玉米',
|
||||||
|
yield: 650,
|
||||||
|
suitabilityScore: 72,
|
||||||
|
suitabilityGrade: '一般适宜',
|
||||||
|
irrigation: '滴灌',
|
||||||
|
drainage: '中等',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-3',
|
||||||
|
name: '南区3号地',
|
||||||
|
area: 38.8,
|
||||||
|
location: '东经120.18°, 北纬30.22°',
|
||||||
|
soilType: '砂土',
|
||||||
|
ph: 8.5,
|
||||||
|
organicMatter: 15,
|
||||||
|
nitrogen: 0.8,
|
||||||
|
phosphorus: 0.6,
|
||||||
|
potassium: 10,
|
||||||
|
soilDepth: 42,
|
||||||
|
slope: 8,
|
||||||
|
currentCrop: '小麦',
|
||||||
|
yield: 480,
|
||||||
|
suitabilityScore: 58,
|
||||||
|
suitabilityGrade: '不适宜',
|
||||||
|
irrigation: '漫灌',
|
||||||
|
drainage: '较差',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-4',
|
||||||
|
name: '北区4号地',
|
||||||
|
area: 55.0,
|
||||||
|
location: '东经120.20°, 北纬30.30°',
|
||||||
|
soilType: '壤土',
|
||||||
|
ph: 6.8,
|
||||||
|
organicMatter: 28,
|
||||||
|
nitrogen: 1.6,
|
||||||
|
phosphorus: 1.0,
|
||||||
|
potassium: 16,
|
||||||
|
soilDepth: 75,
|
||||||
|
slope: 2,
|
||||||
|
currentCrop: '大豆',
|
||||||
|
yield: 380,
|
||||||
|
suitabilityScore: 82,
|
||||||
|
suitabilityGrade: '高度适宜',
|
||||||
|
irrigation: '喷灌',
|
||||||
|
drainage: '良好',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const initialState: MultiDimensionState = {
|
||||||
|
selectedFields: ['field-1', 'field-2'],
|
||||||
|
fieldsData: initialFieldsData,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function multiDimensionReducer(
|
||||||
|
state: MultiDimensionState,
|
||||||
|
action: MultiDimensionAction
|
||||||
|
): MultiDimensionState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_SELECTED_FIELDS':
|
||||||
|
return { ...state, selectedFields: action.payload };
|
||||||
|
|
||||||
|
case 'ADD_FIELD':
|
||||||
|
if (state.selectedFields.length >= 4) {
|
||||||
|
return state; // 最多只能同时对比4个地块
|
||||||
|
}
|
||||||
|
return { ...state, selectedFields: [...state.selectedFields, action.payload] };
|
||||||
|
|
||||||
|
case 'REMOVE_FIELD':
|
||||||
|
if (state.selectedFields.length <= 2) {
|
||||||
|
return state; // 至少需要选择2个地块进行对比
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedFields: state.selectedFields.filter(id => id !== action.payload)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'RESET_FIELDS':
|
||||||
|
return { ...state, selectedFields: ['field-1', 'field-2'] };
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,235 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
import { useReducer } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
multiDimensionReducer,
|
||||||
|
initialState,
|
||||||
|
MultiDimensionAction,
|
||||||
|
MultiDimensionState,
|
||||||
|
FieldData
|
||||||
|
} from './components/multiDimensionReducer';
|
||||||
|
import { FieldSelector } from './components/FieldSelector';
|
||||||
|
import { BasicProperties } from './components/BasicProperties';
|
||||||
|
import { NaturalConditions } from './components/NaturalConditions';
|
||||||
|
import { ManagementStatus } from './components/ManagementStatus';
|
||||||
|
import { SuitabilityEvaluation } from './components/SuitabilityEvaluation';
|
||||||
|
|
||||||
|
export default function MultiDimensionPage() {
|
||||||
|
const [state, dispatch] = useReducer(multiDimensionReducer, initialState);
|
||||||
|
|
||||||
|
// 获取当前选中的对比地块
|
||||||
|
const comparisonFields = state.fieldsData.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
const handleAddField = (fieldId: string) => {
|
||||||
|
if (state.selectedFields.length >= 4) {
|
||||||
|
toast.error('最多只能同时对比4个地块');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch({ type: 'ADD_FIELD', payload: fieldId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveField = (fieldId: string) => {
|
||||||
|
if (state.selectedFields.length <= 2) {
|
||||||
|
toast.error('至少需要选择2个地块进行对比');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch({ type: 'REMOVE_FIELD', payload: fieldId });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成统计分析
|
||||||
|
const generateStatistics = () => {
|
||||||
|
if (comparisonFields.length === 0) return null;
|
||||||
|
|
||||||
|
const totalArea = comparisonFields.reduce((sum, f) => sum + f.area, 0);
|
||||||
|
const avgYield = Math.round(comparisonFields.reduce((sum, f) => sum + f.yield, 0) / comparisonFields.length);
|
||||||
|
const avgScore = Math.round(comparisonFields.reduce((sum, f) => sum + f.suitabilityScore, 0) / comparisonFields.length);
|
||||||
|
const avgOrganic = Math.round(comparisonFields.reduce((sum, f) => sum + f.organicMatter, 0) / comparisonFields.length);
|
||||||
|
const highSuitabilityCount = comparisonFields.filter(f => f.suitabilityGrade === '高度适宜').length;
|
||||||
|
const mediumSuitabilityCount = comparisonFields.filter(f => f.suitabilityGrade === '一般适宜').length;
|
||||||
|
const lowSuitabilityCount = comparisonFields.filter(f => f.suitabilityGrade === '不适宜').length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalArea,
|
||||||
|
avgYield,
|
||||||
|
avgScore,
|
||||||
|
avgOrganic,
|
||||||
|
highSuitabilityCount,
|
||||||
|
mediumSuitabilityCount,
|
||||||
|
lowSuitabilityCount,
|
||||||
|
bestField: comparisonFields.sort((a, b) => b.suitabilityScore - a.suitabilityScore)[0],
|
||||||
|
highestYieldField: comparisonFields.sort((a, b) => b.yield - a.yield)[0],
|
||||||
|
bestOrganicField: comparisonFields.sort((a, b) => b.organicMatter - a.organicMatter)[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const statistics = generateStatistics();
|
||||||
|
|
||||||
export default function IndicatorPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold">指标对比</h2>
|
<div>
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
<h2 className="text-green-800 dark:text-green-200">多维度指标看板</h2>
|
||||||
<p className="text-sm">
|
<p className="text-muted-foreground">
|
||||||
<strong>页面路径:</strong> /land-information/comparison/indicator
|
地块基础属性、自然条件、经营现状与适宜性评价的全面对比分析
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
|
{/* 地块选择器 */}
|
||||||
|
<FieldSelector
|
||||||
|
selectedFields={state.selectedFields}
|
||||||
|
fieldsData={state.fieldsData}
|
||||||
|
onAddField={handleAddField}
|
||||||
|
onRemoveField={handleRemoveField}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 统计概览 */}
|
||||||
|
{statistics && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="p-4 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">对比地块总面积</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600 dark:text-green-400 mb-1">
|
||||||
|
{statistics.totalArea}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-green-700 dark:text-green-300">亩</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-950 dark:to-blue-900 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">平均产量</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400 mb-1">
|
||||||
|
{statistics.avgYield}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-blue-700 dark:text-blue-300">kg/亩</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-950 dark:to-purple-900 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">平均适宜性评分</p>
|
||||||
|
<p className="text-2xl font-bold text-purple-600 dark:text-purple-400 mb-1">
|
||||||
|
{statistics.avgScore}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-purple-700 dark:text-purple-300">分</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-amber-50 to-amber-100 dark:from-amber-950 dark:to-amber-900 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">平均有机质</p>
|
||||||
|
<p className="text-2xl font-bold text-amber-600 dark:text-amber-400 mb-1">
|
||||||
|
{statistics.avgOrganic}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-amber-700 dark:text-amber-300">g/kg</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 适宜性分布统计 */}
|
||||||
|
{statistics && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-950 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">高度适宜</p>
|
||||||
|
<p className="text-xl font-bold text-green-600 dark:text-green-400">
|
||||||
|
{statistics.highSuitabilityCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl text-green-400 opacity-20">📊</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">一般适宜</p>
|
||||||
|
<p className="text-xl font-bold text-yellow-600 dark:text-yellow-400">
|
||||||
|
{statistics.mediumSuitabilityCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl text-yellow-400 opacity-20">📈</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-950 rounded-lg border border-red-200 dark:border-red-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">不适宜</p>
|
||||||
|
<p className="text-xl font-bold text-red-600 dark:text-red-400">
|
||||||
|
{statistics.lowSuitabilityCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl text-red-400 opacity-20">⚠️</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 详细对比表格 */}
|
||||||
|
{comparisonFields.length > 0 && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 基础属性对比 */}
|
||||||
|
<BasicProperties comparisonFields={comparisonFields} />
|
||||||
|
|
||||||
|
{/* 自然条件对比 */}
|
||||||
|
<NaturalConditions comparisonFields={comparisonFields} />
|
||||||
|
|
||||||
|
{/* 经营现状对比 */}
|
||||||
|
<ManagementStatus comparisonFields={comparisonFields} />
|
||||||
|
|
||||||
|
{/* 适宜性评价对比 */}
|
||||||
|
<SuitabilityEvaluation comparisonFields={comparisonFields} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 优秀地块展示 */}
|
||||||
|
{statistics && comparisonFields.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-gradient-to-br from-green-50 to-white dark:from-green-950 dark:to-card rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<h4 className="text-sm font-medium text-green-800 dark:text-green-200 mb-2">🏆 最优地块</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-medium">{statistics.bestField.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">综合评分: {statistics.bestField.suitabilityScore}分</p>
|
||||||
|
<p className="text-xs text-muted-foreground">产量: {statistics.bestField.yield}kg/亩</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-blue-50 to-white dark:from-blue-950 dark:to-card rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
|
<h4 className="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">🌾 产量冠军</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-medium">{statistics.highestYieldField.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">产量: {statistics.highestYieldField.yield}kg/亩</p>
|
||||||
|
<p className="text-xs text-muted-foreground">评分: {statistics.highestYieldField.suitabilityScore}分</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-purple-50 to-white dark:from-purple-950 dark:to-card rounded-lg border border-purple-200 dark:border-purple-800">
|
||||||
|
<h4 className="text-sm font-medium text-purple-800 dark:text-purple-200 mb-2">🌱 肥力最佳</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-medium">{statistics.bestOrganicField.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">有机质: {statistics.bestOrganicField.organicMatter}g/kg</p>
|
||||||
|
<p className="text-xs text-muted-foreground">评分: {statistics.bestOrganicField.suitabilityScore}分</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 空状态 */}
|
||||||
|
{comparisonFields.length === 0 && (
|
||||||
|
<div className="p-12 bg-card rounded-lg border text-center">
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
<p className="text-lg mb-2">暂无对比地块</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
请先选择2-4个地块进行多维度指标对比分析
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Plus, X, FileText } from 'lucide-react';
|
||||||
|
import { ReportComparisonState, FieldData } from './reportComparisonReducer';
|
||||||
|
|
||||||
|
interface FieldSelectorProps {
|
||||||
|
state: ReportComparisonState;
|
||||||
|
onAddField: (fieldId: string) => void;
|
||||||
|
onRemoveField: (fieldId: string) => void;
|
||||||
|
onGenerateReport: () => void;
|
||||||
|
fieldsData: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldSelector({
|
||||||
|
state,
|
||||||
|
onAddField,
|
||||||
|
onRemoveField,
|
||||||
|
onGenerateReport,
|
||||||
|
fieldsData
|
||||||
|
}: FieldSelectorProps) {
|
||||||
|
const availableFields = fieldsData.filter(f => !state.selectedFields.includes(f.id));
|
||||||
|
const comparisonFields = fieldsData.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">选择对比地块 (2-4个)</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<Badge
|
||||||
|
key={field.id}
|
||||||
|
className="bg-green-100 text-green-800 px-3 py-1.5 flex items-center gap-2 font-light"
|
||||||
|
>
|
||||||
|
{field.name}
|
||||||
|
<button
|
||||||
|
onClick={() => onRemoveField(field.id)}
|
||||||
|
className="hover:bg-green-200 rounded-full p-0.5"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{availableFields.length > 0 && (
|
||||||
|
<Select onValueChange={onAddField}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>添加地块</span>
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableFields.map(field => (
|
||||||
|
<SelectItem key={field.id} value={field.id}>
|
||||||
|
{field.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block opacity-0">操作</label>
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={onGenerateReport}
|
||||||
|
disabled={state.reportGenerating}
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
生成报告
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state.reportGenerating && (
|
||||||
|
<div className="mt-4 pt-4 border-t">
|
||||||
|
<div className="flex items-center justify-between text-sm mb-2">
|
||||||
|
<span className="text-muted-foreground">生成进度</span>
|
||||||
|
<span className="font-medium">{state.reportProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={state.reportProgress} className="h-2" />
|
||||||
|
<p className="text-xs text-muted-foreground text-center mt-2">
|
||||||
|
正在生成报告,请稍候...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { FileText, Clock, MapPin, Download, X } from 'lucide-react';
|
||||||
|
import { ComparisonReport } from './reportComparisonReducer';
|
||||||
|
|
||||||
|
interface ReportListProps {
|
||||||
|
savedReports: ComparisonReport[];
|
||||||
|
onDownloadPDF: (report: ComparisonReport) => void;
|
||||||
|
onDownloadWord: (report: ComparisonReport) => void;
|
||||||
|
onDeleteReport: (reportId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReportList({
|
||||||
|
savedReports,
|
||||||
|
onDownloadPDF,
|
||||||
|
onDownloadWord,
|
||||||
|
onDeleteReport
|
||||||
|
}: ReportListProps) {
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-green-600" />
|
||||||
|
历史报告列表
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{savedReports.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<FileText className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>暂无保存的报告</p>
|
||||||
|
<p className="text-sm mt-1">选择地块后点击"生成报告"创建新报告</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{savedReports.map((report) => (
|
||||||
|
<div
|
||||||
|
key={report.id}
|
||||||
|
className="p-4 border rounded-lg hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="mb-2 flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-green-600" />
|
||||||
|
{report.name}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
生成时间: {report.generatedTime}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MapPin className="w-3 h-3" />
|
||||||
|
对比地块: {report.comparedFields.join('、')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 ml-4">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
onClick={() => onDownloadPDF(report)}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-1" />
|
||||||
|
下载PDF
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||||||
|
onClick={() => onDownloadWord(report)}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-1" />
|
||||||
|
下载Word
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
|
||||||
|
onClick={() => onDeleteReport(report.id)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Eye, Zap, Lightbulb, Target, ArrowRight, CheckCircle2, Download } from 'lucide-react';
|
||||||
|
import { FieldData } from './reportComparisonReducer';
|
||||||
|
|
||||||
|
interface ReportPreviewProps {
|
||||||
|
comparisonFields: FieldData[];
|
||||||
|
onDownloadPDF: () => void;
|
||||||
|
onDownloadWord: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReportPreview({
|
||||||
|
comparisonFields,
|
||||||
|
onDownloadPDF,
|
||||||
|
onDownloadWord
|
||||||
|
}: ReportPreviewProps) {
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return 'bg-green-500 text-white';
|
||||||
|
case '一般适宜': return 'bg-yellow-500 text-white';
|
||||||
|
case '不适宜': return 'bg-red-500 text-white';
|
||||||
|
default: return 'bg-gray-500 text-white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成智能分析结论
|
||||||
|
const generateAnalysis = () => {
|
||||||
|
const analyses: string[] = [];
|
||||||
|
|
||||||
|
// 对比产量和有机质
|
||||||
|
const sortedByYield = [...comparisonFields].sort((a, b) => b.yield - a.yield);
|
||||||
|
const highestYield = sortedByYield[0];
|
||||||
|
const lowestYield = sortedByYield[sortedByYield.length - 1];
|
||||||
|
|
||||||
|
if (highestYield.organicMatter > lowestYield.organicMatter) {
|
||||||
|
const diff = ((highestYield.organicMatter - lowestYield.organicMatter) / lowestYield.organicMatter * 100).toFixed(1);
|
||||||
|
analyses.push(
|
||||||
|
`${highestYield.name}产量高于${lowestYield.name},主要原因是其有机质含量高出${diff}%,建议${lowestYield.name}增施有机肥。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比pH值
|
||||||
|
const acidFields = comparisonFields.filter(f => f.ph < 6.0);
|
||||||
|
const alkalineFields = comparisonFields.filter(f => f.ph > 7.5);
|
||||||
|
|
||||||
|
if (acidFields.length > 0) {
|
||||||
|
analyses.push(
|
||||||
|
`${acidFields.map(f => f.name).join('、')}的pH值偏低,建议施用石灰或碱性肥料改良。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alkalineFields.length > 0) {
|
||||||
|
analyses.push(
|
||||||
|
`${alkalineFields.map(f => f.name).join('、')}的pH值偏高,建议施用硫磺或酸性肥料降低pH值。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比土层厚度
|
||||||
|
const shallowFields = comparisonFields.filter(f => f.soilDepth < 60);
|
||||||
|
if (shallowFields.length > 0) {
|
||||||
|
analyses.push(
|
||||||
|
`${shallowFields.map(f => f.name).join('、')}的土层厚度不足,建议进行深耕深松,提高土壤蓄水保肥能力。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比适宜性
|
||||||
|
const highSuitability = comparisonFields.filter(f => f.suitabilityScore >= 80);
|
||||||
|
const lowSuitability = comparisonFields.filter(f => f.suitabilityScore < 60);
|
||||||
|
|
||||||
|
if (highSuitability.length > 0 && lowSuitability.length > 0) {
|
||||||
|
analyses.push(
|
||||||
|
`${highSuitability.map(f => f.name).join('、')}综合适宜性较高,可优先种植高价值经济作物;${lowSuitability.map(f => f.name).join('、')}需要进行系统改良。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return analyses;
|
||||||
|
};
|
||||||
|
|
||||||
|
const smartAnalyses = generateAnalysis();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 报告预览 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<Eye className="w-5 h-5 text-blue-600" />
|
||||||
|
当前对比分析报告预览
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* 报告标题 */}
|
||||||
|
<div className="mb-6 p-4 bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-950 dark:to-blue-950 rounded-lg border-l-4 border-green-600">
|
||||||
|
<h2 className="text-green-800 dark:text-green-200 mb-2">地块对比分析报告</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
预览时间: {new Date().toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}).replace(/\//g, '-')} | 对比地块: {comparisonFields.map(f => f.name).join('、')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 执行摘要 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4 text-yellow-600" />
|
||||||
|
执行摘要
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">最优地块</p>
|
||||||
|
<p className="text-lg font-medium text-green-700 dark:text-green-300">
|
||||||
|
{[...comparisonFields].sort((a, b) => b.suitabilityScore - a.suitabilityScore)[0].name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
综合评分: {[...comparisonFields].sort((a, b) => b.suitabilityScore - a.suitabilityScore)[0].suitabilityScore}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">最高产量</p>
|
||||||
|
<p className="text-lg font-medium text-blue-700 dark:text-blue-300">
|
||||||
|
{[...comparisonFields].sort((a, b) => b.yield - a.yield)[0].name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{[...comparisonFields].sort((a, b) => b.yield - a.yield)[0].yield} kg/亩
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">最佳有机质</p>
|
||||||
|
<p className="text-lg font-medium text-purple-700 dark:text-purple-300">
|
||||||
|
{[...comparisonFields].sort((a, b) => b.organicMatter - a.organicMatter)[0].name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{[...comparisonFields].sort((a, b) => b.organicMatter - a.organicMatter)[0].organicMatter} g/kg
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 智能分析结论 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<Lightbulb className="w-4 h-4 text-yellow-600" />
|
||||||
|
智能分析结论
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{smartAnalyses.map((analysis, index) => (
|
||||||
|
<div key={index} className="p-4 bg-blue-50 dark:bg-blue-950 border-l-4 border-blue-600 rounded-r-lg">
|
||||||
|
<p className="text-sm text-blue-900 dark:text-blue-100 flex items-start gap-2">
|
||||||
|
<ArrowRight className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>{analysis}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 改进建议 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<Target className="w-4 h-4 text-green-600" />
|
||||||
|
改进建议
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{comparisonFields.map(field => {
|
||||||
|
const suggestions: string[] = [];
|
||||||
|
|
||||||
|
if (field.ph < 6.0 || field.ph > 7.5) {
|
||||||
|
suggestions.push(
|
||||||
|
field.ph < 6.0
|
||||||
|
? '施用石灰或碱性肥料提高pH值至6.0-7.5'
|
||||||
|
: '施用硫磺或酸性肥料降低pH值至6.0-7.5'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.organicMatter < 25) {
|
||||||
|
suggestions.push('增施有机肥,提高有机质含量至25 g/kg以上');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.soilDepth < 60) {
|
||||||
|
suggestions.push('进行深耕深松,改善土层厚度');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.nitrogen < 1.5) {
|
||||||
|
suggestions.push('适量施用氮肥,提高全氮含量');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggestions.length === 0) {
|
||||||
|
suggestions.push('土壤条件良好,继续保持现有管理措施');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<h5 className="text-sm font-medium text-green-800 dark:text-green-200 mb-2">{field.name}</h5>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{suggestions.map((suggestion, i) => (
|
||||||
|
<li key={i} className="text-sm text-green-700 dark:text-green-300 flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>{suggestion}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数据表格 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="mb-3">详细数据表格</h4>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full border">
|
||||||
|
<thead className="bg-gray-100 dark:bg-gray-900">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left text-xs border">地块名称</th>
|
||||||
|
<th className="px-3 py-2 text-center text-xs border">面积</th>
|
||||||
|
<th className="px-3 py-2 text-center text-xs border">pH</th>
|
||||||
|
<th className="px-3 py-2 text-center text-xs border">有机质</th>
|
||||||
|
<th className="px-3 py-2 text-center text-xs border">产量</th>
|
||||||
|
<th className="px-3 py-2 text-center text-xs border">适宜性</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{comparisonFields.map(field => (
|
||||||
|
<tr key={field.id} className="border-t">
|
||||||
|
<td className="px-3 py-2 text-sm border">{field.name}</td>
|
||||||
|
<td className="px-3 py-2 text-center text-sm border">{field.area}亩</td>
|
||||||
|
<td className="px-3 py-2 text-center text-sm border">{field.ph}</td>
|
||||||
|
<td className="px-3 py-2 text-center text-sm border">{field.organicMatter} g/kg</td>
|
||||||
|
<td className="px-3 py-2 text-center text-sm border">{field.yield} kg/亩</td>
|
||||||
|
<td className="px-3 py-2 text-center text-sm border">
|
||||||
|
<Badge className={getGradeColor(field.suitabilityGrade)}>
|
||||||
|
{field.suitabilityGrade}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图表插入位置 */}
|
||||||
|
<div className="mb-6 p-4 bg-muted rounded-lg border-2 border-dashed border-gray-300">
|
||||||
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
|
📊 报告中将自动插入雷达图、柱状图等可视化图表
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 下载当前报告 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2">下载当前对比报告</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
将当前预览的报告导出为PDF或Word格式
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
onClick={onDownloadPDF}
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5 mr-2" />
|
||||||
|
下载PDF报告
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={onDownloadWord}
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5 mr-2" />
|
||||||
|
下载Word报告
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-900 dark:text-blue-100">
|
||||||
|
💡 <strong>报告内容包含:</strong>
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 text-sm text-blue-800 dark:text-blue-200 space-y-1">
|
||||||
|
<li>• 执行摘要与关键指标对比</li>
|
||||||
|
<li>• 多维度指标对比表格</li>
|
||||||
|
<li>• 雷达图、柱状图等可视化图表</li>
|
||||||
|
<li>• 基于规则引擎的智能分析结论</li>
|
||||||
|
<li>• 针对性改进建议与措施</li>
|
||||||
|
<li>• 地块空间分布对比图</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface FieldData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
area: number;
|
||||||
|
location: string;
|
||||||
|
soilType: string;
|
||||||
|
ph: number;
|
||||||
|
organicMatter: number;
|
||||||
|
nitrogen: number;
|
||||||
|
phosphorus: number;
|
||||||
|
potassium: number;
|
||||||
|
soilDepth: number;
|
||||||
|
slope: number;
|
||||||
|
currentCrop: string;
|
||||||
|
yield: number;
|
||||||
|
suitabilityScore: number;
|
||||||
|
suitabilityGrade: '高度适宜' | '一般适宜' | '不适宜';
|
||||||
|
irrigation: string;
|
||||||
|
drainage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComparisonReport {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
generatedTime: string;
|
||||||
|
comparedFields: string[];
|
||||||
|
fieldData: FieldData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportComparisonState {
|
||||||
|
selectedFields: string[];
|
||||||
|
reportGenerating: boolean;
|
||||||
|
reportProgress: number;
|
||||||
|
savedReports: ComparisonReport[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReportComparisonAction =
|
||||||
|
| { type: 'SET_SELECTED_FIELDS'; payload: string[] }
|
||||||
|
| { type: 'ADD_FIELD'; payload: string }
|
||||||
|
| { type: 'REMOVE_FIELD'; payload: string }
|
||||||
|
| { type: 'SET_REPORT_GENERATING'; payload: boolean }
|
||||||
|
| { type: 'SET_REPORT_PROGRESS'; payload: number }
|
||||||
|
| { type: 'ADD_SAVED_REPORT'; payload: ComparisonReport }
|
||||||
|
| { type: 'DELETE_REPORT'; payload: string };
|
||||||
|
|
||||||
|
const fieldsData: FieldData[] = [
|
||||||
|
{
|
||||||
|
id: 'field-1',
|
||||||
|
name: '东区1号地',
|
||||||
|
area: 50.5,
|
||||||
|
location: '东经120.15°, 北纬30.25°',
|
||||||
|
soilType: '壤土',
|
||||||
|
ph: 6.5,
|
||||||
|
organicMatter: 32,
|
||||||
|
nitrogen: 1.8,
|
||||||
|
phosphorus: 1.2,
|
||||||
|
potassium: 18,
|
||||||
|
soilDepth: 85,
|
||||||
|
slope: 3,
|
||||||
|
currentCrop: '水稻',
|
||||||
|
yield: 750,
|
||||||
|
suitabilityScore: 87,
|
||||||
|
suitabilityGrade: '高度适宜',
|
||||||
|
irrigation: '喷灌',
|
||||||
|
drainage: '良好',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-2',
|
||||||
|
name: '西区2号地',
|
||||||
|
area: 45.2,
|
||||||
|
location: '东经120.12°, 北纬30.28°',
|
||||||
|
soilType: '粘土',
|
||||||
|
ph: 7.8,
|
||||||
|
organicMatter: 22,
|
||||||
|
nitrogen: 1.3,
|
||||||
|
phosphorus: 0.9,
|
||||||
|
potassium: 14,
|
||||||
|
soilDepth: 55,
|
||||||
|
slope: 5,
|
||||||
|
currentCrop: '玉米',
|
||||||
|
yield: 650,
|
||||||
|
suitabilityScore: 72,
|
||||||
|
suitabilityGrade: '一般适宜',
|
||||||
|
irrigation: '滴灌',
|
||||||
|
drainage: '中等',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-3',
|
||||||
|
name: '南区3号地',
|
||||||
|
area: 38.8,
|
||||||
|
location: '东经120.18°, 北纬30.22°',
|
||||||
|
soilType: '砂土',
|
||||||
|
ph: 8.5,
|
||||||
|
organicMatter: 15,
|
||||||
|
nitrogen: 0.8,
|
||||||
|
phosphorus: 0.6,
|
||||||
|
potassium: 10,
|
||||||
|
soilDepth: 42,
|
||||||
|
slope: 8,
|
||||||
|
currentCrop: '小麦',
|
||||||
|
yield: 480,
|
||||||
|
suitabilityScore: 58,
|
||||||
|
suitabilityGrade: '不适宜',
|
||||||
|
irrigation: '漫灌',
|
||||||
|
drainage: '较差',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-4',
|
||||||
|
name: '北区4号地',
|
||||||
|
area: 55.0,
|
||||||
|
location: '东经120.20°, 北纬30.30°',
|
||||||
|
soilType: '壤土',
|
||||||
|
ph: 6.8,
|
||||||
|
organicMatter: 28,
|
||||||
|
nitrogen: 1.6,
|
||||||
|
phosphorus: 1.0,
|
||||||
|
potassium: 16,
|
||||||
|
soilDepth: 75,
|
||||||
|
slope: 2,
|
||||||
|
currentCrop: '大豆',
|
||||||
|
yield: 380,
|
||||||
|
suitabilityScore: 82,
|
||||||
|
suitabilityGrade: '高度适宜',
|
||||||
|
irrigation: '喷灌',
|
||||||
|
drainage: '良好',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialState: ReportComparisonState = {
|
||||||
|
selectedFields: ['field-1', 'field-2'],
|
||||||
|
reportGenerating: false,
|
||||||
|
reportProgress: 0,
|
||||||
|
savedReports: [
|
||||||
|
{
|
||||||
|
id: 'report-1',
|
||||||
|
name: '东区1号地 vs 西区2号地对比分析',
|
||||||
|
generatedTime: '2024-10-15 14:35:22',
|
||||||
|
comparedFields: ['东区1号地', '西区2号地'],
|
||||||
|
fieldData: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'report-2',
|
||||||
|
name: '东区1号地 vs 西区2号地 vs 南区3号地对比分析',
|
||||||
|
generatedTime: '2024-10-14 09:20:15',
|
||||||
|
comparedFields: ['东区1号地', '西区2号地', '南区3号地'],
|
||||||
|
fieldData: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function reportComparisonReducer(
|
||||||
|
state: ReportComparisonState = initialState,
|
||||||
|
action: ReportComparisonAction
|
||||||
|
): ReportComparisonState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_SELECTED_FIELDS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedFields: action.payload,
|
||||||
|
};
|
||||||
|
case 'ADD_FIELD':
|
||||||
|
if (state.selectedFields.length >= 4) {
|
||||||
|
toast.error('最多只能同时对比4个地块');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedFields: [...state.selectedFields, action.payload],
|
||||||
|
};
|
||||||
|
case 'REMOVE_FIELD':
|
||||||
|
if (state.selectedFields.length <= 2) {
|
||||||
|
toast.error('至少需要选择2个地块进行对比');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedFields: state.selectedFields.filter(id => id !== action.payload),
|
||||||
|
};
|
||||||
|
case 'SET_REPORT_GENERATING':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
reportGenerating: action.payload,
|
||||||
|
};
|
||||||
|
case 'SET_REPORT_PROGRESS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
reportProgress: action.payload,
|
||||||
|
};
|
||||||
|
case 'ADD_SAVED_REPORT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
savedReports: [action.payload, ...state.savedReports],
|
||||||
|
};
|
||||||
|
case 'DELETE_REPORT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
savedReports: state.savedReports.filter(r => r.id !== action.payload),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fieldsData, initialState };
|
||||||
@@ -1,18 +1,307 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
import { useReducer } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { FileText, Download } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
reportComparisonReducer,
|
||||||
|
initialState,
|
||||||
|
ReportComparisonState,
|
||||||
|
ComparisonReport,
|
||||||
|
fieldsData
|
||||||
|
} from './components/reportComparisonReducer';
|
||||||
|
import { FieldSelector } from './components/FieldSelector';
|
||||||
|
import { ReportList } from './components/ReportList';
|
||||||
|
import { ReportPreview } from './components/ReportPreview';
|
||||||
|
|
||||||
export default function ReportPage() {
|
export default function ReportPage() {
|
||||||
|
const [state, dispatch] = useReducer(reportComparisonReducer, initialState);
|
||||||
|
|
||||||
|
const comparisonFields = fieldsData.filter(f => state.selectedFields.includes(f.id));
|
||||||
|
|
||||||
|
const handleAddField = (fieldId: string) => {
|
||||||
|
dispatch({ type: 'ADD_FIELD', payload: fieldId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveField = (fieldId: string) => {
|
||||||
|
dispatch({ type: 'REMOVE_FIELD', payload: fieldId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateReport = () => {
|
||||||
|
dispatch({ type: 'SET_REPORT_GENERATING', payload: true });
|
||||||
|
dispatch({ type: 'SET_REPORT_PROGRESS', payload: 0 });
|
||||||
|
|
||||||
|
let progress = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
progress += 10;
|
||||||
|
dispatch({ type: 'SET_REPORT_PROGRESS', payload: progress });
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
dispatch({ type: 'SET_REPORT_GENERATING', payload: false });
|
||||||
|
|
||||||
|
// 保存新报告
|
||||||
|
const reportName = comparisonFields.map(f => f.name).join(' vs ') + '对比分析';
|
||||||
|
const newReport: ComparisonReport = {
|
||||||
|
id: `report-${Date.now()}`,
|
||||||
|
name: reportName,
|
||||||
|
generatedTime: new Date().toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}).replace(/\//g, '-'),
|
||||||
|
comparedFields: comparisonFields.map(f => f.name),
|
||||||
|
fieldData: comparisonFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({ type: 'ADD_SAVED_REPORT', payload: newReport });
|
||||||
|
toast.success('对比分析报告已生成!');
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadPDF = async (report?: ComparisonReport) => {
|
||||||
|
const reportData = report || {
|
||||||
|
name: comparisonFields.map(f => f.name).join(' vs ') + '对比分析',
|
||||||
|
generatedTime: new Date().toLocaleString('zh-CN'),
|
||||||
|
comparedFields: comparisonFields.map(f => f.name),
|
||||||
|
fieldData: comparisonFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
toast.info('正在生成PDF报告,请稍候...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 简化的PDF生成逻辑(实际项目中可使用jsPDF等库)
|
||||||
|
const pdfContent = generatePDFContent(reportData);
|
||||||
|
downloadFile(pdfContent, `${reportData.name}_${reportData.generatedTime.replace(/:/g, '-').replace(/\//g, '-')}.pdf`, 'application/pdf');
|
||||||
|
toast.success('PDF报告已下载!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PDF生成失败:', error);
|
||||||
|
toast.error('PDF生成失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadWord = async (report?: ComparisonReport) => {
|
||||||
|
const reportData = report || {
|
||||||
|
name: comparisonFields.map(f => f.name).join(' vs ') + '对比分析',
|
||||||
|
generatedTime: new Date().toLocaleString('zh-CN'),
|
||||||
|
comparedFields: comparisonFields.map(f => f.name),
|
||||||
|
fieldData: comparisonFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
toast.info('正在生成Word报告,请稍候...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fields = reportData.fieldData.length > 0 ? reportData.fieldData : comparisonFields;
|
||||||
|
const bestField = [...fields].sort((a, b) => b.suitabilityScore - a.suitabilityScore)[0];
|
||||||
|
const highestYield = [...fields].sort((a, b) => b.yield - a.yield)[0];
|
||||||
|
const bestOrganic = [...fields].sort((a, b) => b.organicMatter - a.organicMatter)[0];
|
||||||
|
|
||||||
|
// 创建HTML格式的Word文档
|
||||||
|
const htmlContent = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: "Microsoft YaHei", Arial, sans-serif; padding: 40px; }
|
||||||
|
h1 { text-align: center; color: #2d5016; font-size: 24px; margin-bottom: 30px; }
|
||||||
|
h2 { color: #3f7f1f; font-size: 18px; margin-top: 30px; margin-bottom: 15px; border-bottom: 2px solid #3f7f1f; padding-bottom: 5px; }
|
||||||
|
.info { margin: 20px 0; color: #666; font-size: 14px; }
|
||||||
|
.summary { background: #f0f9ff; padding: 20px; border-left: 4px solid #3f7f1f; margin: 20px 0; }
|
||||||
|
.summary-item { margin: 10px 0; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||||
|
th { background-color: #3f7f1f; color: white; }
|
||||||
|
tr:nth-child(even) { background-color: #f9f9f9; }
|
||||||
|
.analysis { background: #e8f5e9; padding: 15px; margin: 10px 0; border-radius: 4px; }
|
||||||
|
.footer { margin-top: 40px; text-align: center; color: #999; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>地块对比分析报告</h1>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<p><strong>生成时间:</strong>${reportData.generatedTime}</p>
|
||||||
|
<p><strong>对比地块:</strong>${reportData.comparedFields.join('、')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>执行摘要</h2>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="summary-item">
|
||||||
|
<strong>最优地块:</strong>${bestField?.name || 'N/A'}
|
||||||
|
<span style="color: #3f7f1f;">(综合评分: ${bestField?.suitabilityScore || 0})</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<strong>最高产量:</strong>${highestYield?.name || 'N/A'}
|
||||||
|
<span style="color: #3f7f1f;">(${highestYield?.yield || 0} kg/亩)</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<strong>最佳有机质:</strong>${bestOrganic?.name || 'N/A'}
|
||||||
|
<span style="color: #3f7f1f;">(${bestOrganic?.organicMatter || 0} g/kg)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>详细数据对比</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>地块名称</th>
|
||||||
|
<th>面积(亩)</th>
|
||||||
|
<th>土壤类型</th>
|
||||||
|
<th>pH值</th>
|
||||||
|
<th>有机质(g/kg)</th>
|
||||||
|
<th>产量(kg/亩)</th>
|
||||||
|
<th>适宜性</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${fields.map(field => `
|
||||||
|
<tr>
|
||||||
|
<td>${field.name}</td>
|
||||||
|
<td>${field.area}</td>
|
||||||
|
<td>${field.soilType}</td>
|
||||||
|
<td>${field.ph}</td>
|
||||||
|
<td>${field.organicMatter}</td>
|
||||||
|
<td>${field.yield}</td>
|
||||||
|
<td>${field.suitabilityGrade}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>智能分析结论</h2>
|
||||||
|
<div class="analysis">
|
||||||
|
<p>• 根据对比分析,<strong>${bestField?.name}</strong>综合适宜性最高,建议优先发展。</p>
|
||||||
|
<p>• <strong>${highestYield?.name}</strong>产量表现最优,可作为高产示范田。</p>
|
||||||
|
<p>• <strong>${bestOrganic?.name}</strong>有机质含量最佳,土壤肥力较好。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>智慧农业生产管理系统 - 地块对比分析报告</p>
|
||||||
|
<p>本报告由系统自动生成</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob(['\ufeff', htmlContent], {
|
||||||
|
type: 'application/msword'
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${reportData.name}_${reportData.generatedTime.replace(/:/g, '-').replace(/\//g, '-')}.doc`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
toast.success('Word报告已下载!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Word生成失败:', error);
|
||||||
|
toast.error('Word生成失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteReport = (reportId: string) => {
|
||||||
|
dispatch({ type: 'DELETE_REPORT', payload: reportId });
|
||||||
|
toast.success('报告已删除');
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePDFContent = (reportData: any) => {
|
||||||
|
// 简化的PDF内容生成
|
||||||
|
const fields = reportData.fieldData.length > 0 ? reportData.fieldData : comparisonFields;
|
||||||
|
const bestField = [...fields].sort((a, b) => b.suitabilityScore - a.suitabilityScore)[0];
|
||||||
|
const highestYield = [...fields].sort((a, b) => b.yield - a.yield)[0];
|
||||||
|
|
||||||
|
return `
|
||||||
|
地块对比分析报告
|
||||||
|
================
|
||||||
|
|
||||||
|
生成时间: ${reportData.generatedTime}
|
||||||
|
对比地块: ${reportData.comparedFields.join('、')}
|
||||||
|
|
||||||
|
执行摘要
|
||||||
|
--------
|
||||||
|
最优地块: ${bestField?.name || 'N/A'} (综合评分: ${bestField?.suitabilityScore || 0})
|
||||||
|
最高产量: ${highestYield?.name || 'N/A'} (${highestYield?.yield || 0} kg/亩)
|
||||||
|
|
||||||
|
详细数据
|
||||||
|
--------
|
||||||
|
${fields.map(field => `
|
||||||
|
${field.name}:
|
||||||
|
- 面积: ${field.area}亩
|
||||||
|
- pH值: ${field.ph}
|
||||||
|
- 有机质: ${field.organicMatter} g/kg
|
||||||
|
- 产量: ${field.yield} kg/亩
|
||||||
|
- 适宜性: ${field.suitabilityGrade}
|
||||||
|
`).join('\n')}
|
||||||
|
|
||||||
|
分析结论
|
||||||
|
--------
|
||||||
|
根据对比分析,${bestField?.name}综合适宜性最高,建议优先发展。
|
||||||
|
${highestYield?.name}产量表现最优,可作为高产示范田。
|
||||||
|
`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = (content: string, filename: string, contentType: string) => {
|
||||||
|
const blob = new Blob([content], { type: contentType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold">报告对比</h2>
|
<div>
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
<h2 className="text-green-800 dark:text-green-200">报告对比生成</h2>
|
||||||
<p className="text-sm">
|
<p className="text-muted-foreground">
|
||||||
<strong>页面路径:</strong> /land-information/comparison/report
|
智能报告生成、多维度对比分析与一键导出
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => handleDownloadPDF()}>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
快速生成PDF
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地块选择器 */}
|
||||||
|
<FieldSelector
|
||||||
|
state={state}
|
||||||
|
onAddField={handleAddField}
|
||||||
|
onRemoveField={handleRemoveField}
|
||||||
|
onGenerateReport={handleGenerateReport}
|
||||||
|
fieldsData={fieldsData}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 历史报告列表 */}
|
||||||
|
<ReportList
|
||||||
|
savedReports={state.savedReports}
|
||||||
|
onDownloadPDF={handleDownloadPDF}
|
||||||
|
onDownloadWord={handleDownloadWord}
|
||||||
|
onDeleteReport={handleDeleteReport}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 报告预览 */}
|
||||||
|
<ReportPreview
|
||||||
|
comparisonFields={comparisonFields}
|
||||||
|
onDownloadPDF={() => handleDownloadPDF()}
|
||||||
|
onDownloadWord={() => handleDownloadWord()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, LineChart, Line } from 'recharts';
|
||||||
|
import { BarChart3, PieChart as PieChartIcon, Target, TrendingUp } from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface AnalysisChartsProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnalysisCharts({ state, dispatch }: AnalysisChartsProps) {
|
||||||
|
// 准备图表数据
|
||||||
|
const scoreDistributionData = [
|
||||||
|
{ range: '0-20分', count: state.analysisResults.filter(r => r.overallScore < 20).length, color: '#dc2626' },
|
||||||
|
{ range: '20-40分', count: state.analysisResults.filter(r => r.overallScore >= 20 && r.overallScore < 40).length, color: '#ea580c' },
|
||||||
|
{ range: '40-60分', count: state.analysisResults.filter(r => r.overallScore >= 40 && r.overallScore < 60).length, color: '#f59e0b' },
|
||||||
|
{ range: '60-80分', count: state.analysisResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length, color: '#22c55e' },
|
||||||
|
{ range: '80-100分', count: state.analysisResults.filter(r => r.overallScore >= 80).length, color: '#16a34a' }
|
||||||
|
].filter(item => item.count > 0);
|
||||||
|
|
||||||
|
const factorPerformanceData = state.factors
|
||||||
|
.filter(f => f.enabled)
|
||||||
|
.map(factor => ({
|
||||||
|
name: factor.name,
|
||||||
|
weight: Math.round(factor.weight * 100),
|
||||||
|
category: factor.category
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.weight - a.weight);
|
||||||
|
|
||||||
|
const categoryPerformanceData = [
|
||||||
|
{
|
||||||
|
category: '土壤条件',
|
||||||
|
avgScore: state.analysisResults.length > 0
|
||||||
|
? Math.round(state.analysisResults.reduce((sum, r) => sum + r.soilScore, 0) / state.analysisResults.length)
|
||||||
|
: 0,
|
||||||
|
weight: Math.round(state.weightConfig.soil * 100)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '气候条件',
|
||||||
|
avgScore: state.analysisResults.length > 0
|
||||||
|
? Math.round(state.analysisResults.reduce((sum, r) => sum + r.climateScore, 0) / state.analysisResults.length)
|
||||||
|
: 0,
|
||||||
|
weight: Math.round(state.weightConfig.climate * 100)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '地形条件',
|
||||||
|
avgScore: state.analysisResults.length > 0
|
||||||
|
? Math.round(state.analysisResults.reduce((sum, r) => sum + r.topographyScore, 0) / state.analysisResults.length)
|
||||||
|
: 0,
|
||||||
|
weight: Math.round(state.weightConfig.topography * 100)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '基础设施',
|
||||||
|
avgScore: state.analysisResults.length > 0
|
||||||
|
? Math.round(state.analysisResults.reduce((sum, r) => sum + r.infrastructureScore, 0) / state.analysisResults.length)
|
||||||
|
: 0,
|
||||||
|
weight: Math.round(state.weightConfig.infrastructure * 100)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const cropRecommendationData = state.analysisResults
|
||||||
|
.flatMap(result => result.recommendedCrops.slice(0, 3))
|
||||||
|
.reduce((acc, rec) => {
|
||||||
|
const existing = acc.find(item => item.crop === rec.crop.name);
|
||||||
|
if (existing) {
|
||||||
|
existing.frequency += 1;
|
||||||
|
existing.avgScore = Math.round((existing.avgScore + rec.suitabilityScore) / 2);
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
crop: rec.crop.name,
|
||||||
|
frequency: 1,
|
||||||
|
avgScore: rec.suitabilityScore,
|
||||||
|
category: rec.crop.category
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as any[])
|
||||||
|
.sort((a, b) => b.frequency - a.frequency)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
const economicReturnData = state.analysisResults
|
||||||
|
.map(result => {
|
||||||
|
const block = state.landBlocks.find(b => b.id === result.blockId);
|
||||||
|
return {
|
||||||
|
name: result.blockName,
|
||||||
|
returnPerAcre: result.economicReturn && block?.area
|
||||||
|
? Math.round(result.economicReturn / block.area)
|
||||||
|
: 0,
|
||||||
|
totalReturn: result.economicReturn || 0,
|
||||||
|
score: result.overallScore
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.returnPerAcre - a.returnPerAcre);
|
||||||
|
|
||||||
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'];
|
||||||
|
|
||||||
|
if (state.analysisResults.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<BarChart3 className="w-16 h-16 mx-auto mb-4 text-muted-foreground opacity-50" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">暂无图表数据</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
请先运行分析生成结果
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Tabs defaultValue="distribution" className="space-y-4">
|
||||||
|
<TabsList className="grid w-full grid-cols-5">
|
||||||
|
<TabsTrigger value="distribution" className="flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
得分分布
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="factors" className="flex items-center gap-2">
|
||||||
|
<Target className="w-4 h-4" />
|
||||||
|
因子分析
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="crops" className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
作物推荐
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="economic" className="flex items-center gap-2">
|
||||||
|
<PieChartIcon className="w-4 h-4" />
|
||||||
|
经济效益
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="comparison" className="flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
对比分析
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 得分分布 */}
|
||||||
|
<TabsContent value="distribution" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">得分分布直方图</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={scoreDistributionData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="range" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="count" fill="#8884d8">
|
||||||
|
{scoreDistributionData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">等级分布饼图</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={scoreDistributionData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={({ range, count }) => `${range}: ${count}个`}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="count"
|
||||||
|
>
|
||||||
|
{scoreDistributionData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 因子分析 */}
|
||||||
|
<TabsContent value="factors" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">评价因子权重分布</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={factorPerformanceData} layout="horizontal">
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={80} />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="weight" fill="#8884d8" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">各维度得分对比</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<RadarChart data={categoryPerformanceData}>
|
||||||
|
<PolarGrid />
|
||||||
|
<PolarAngleAxis dataKey="category" />
|
||||||
|
<PolarRadiusAxis angle={90} domain={[0, 100]} />
|
||||||
|
<Radar name="平均得分" dataKey="avgScore" stroke="#8884d8" fill="#8884d8" fillOpacity={0.6} />
|
||||||
|
<Radar name="权重" dataKey="weight" stroke="#82ca9d" fill="#82ca9d" fillOpacity={0.6} />
|
||||||
|
<Tooltip />
|
||||||
|
</RadarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 作物推荐 */}
|
||||||
|
<TabsContent value="crops" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">作物推荐频率</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={cropRecommendationData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="crop" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="frequency" fill="#00C49F" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">作物适宜性得分</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={cropRecommendationData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="crop" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="avgScore" fill="#FFBB28" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 经济效益 */}
|
||||||
|
<TabsContent value="economic" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">地块亩均收益</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={economicReturnData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="returnPerAcre" fill="#8884d8" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">得分与收益相关性</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={economicReturnData.sort((a, b) => a.score - b.score)}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="score" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Line type="monotone" dataKey="returnPerAcre" stroke="#8884d8" strokeWidth={2} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 对比分析 */}
|
||||||
|
<TabsContent value="comparison" className="space-y-4">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">地块综合对比</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<BarChart data={state.analysisResults.map(result => ({
|
||||||
|
name: result.blockName,
|
||||||
|
综合得分: result.overallScore,
|
||||||
|
土壤得分: Math.round(result.soilScore),
|
||||||
|
气候得分: Math.round(result.climateScore),
|
||||||
|
地形得分: Math.round(result.topographyScore),
|
||||||
|
基础设施得分: Math.round(result.infrastructureScore)
|
||||||
|
}))}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="综合得分" fill="#8884d8" />
|
||||||
|
<Bar dataKey="土壤得分" fill="#00C49F" />
|
||||||
|
<Bar dataKey="气候得分" fill="#FFBB28" />
|
||||||
|
<Bar dataKey="地形得分" fill="#FF8042" />
|
||||||
|
<Bar dataKey="基础设施得分" fill="#8884d8" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import {
|
||||||
|
Settings,
|
||||||
|
Sliders,
|
||||||
|
BarChart3,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
|
TrendingUp,
|
||||||
|
Info
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface AnalysisControlPanelProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnalysisControlPanel({ state, dispatch }: AnalysisControlPanelProps) {
|
||||||
|
const handleQuickAnalysis = () => {
|
||||||
|
// 快速分析,使用默认设置
|
||||||
|
const allBlocks = state.landBlocks.map(b => b.id);
|
||||||
|
dispatch({ type: 'SET_SELECTED_BLOCKS', payload: allBlocks });
|
||||||
|
// 触发分析
|
||||||
|
setTimeout(() => {
|
||||||
|
const event = new CustomEvent('run-analysis');
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetFilters = () => {
|
||||||
|
dispatch({ type: 'RESET_FILTERS' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabledFactorsCount = state.factors.filter(f => f.enabled).length;
|
||||||
|
const totalWeight = state.weightConfig.soil + state.weightConfig.climate +
|
||||||
|
state.weightConfig.topography + state.weightConfig.infrastructure;
|
||||||
|
|
||||||
|
// 获取风险等级
|
||||||
|
const getRiskLevel = () => {
|
||||||
|
if (state.analysisResults.length === 0) return { level: 'unknown', color: 'gray', text: '未分析' };
|
||||||
|
|
||||||
|
const avgScore = state.analysisResults.reduce((sum, r) => sum + r.overallScore, 0) / state.analysisResults.length;
|
||||||
|
|
||||||
|
if (avgScore >= 80) return { level: 'low', color: 'green', text: '低风险' };
|
||||||
|
if (avgScore >= 60) return { level: 'medium', color: 'yellow', text: '中等风险' };
|
||||||
|
return { level: 'high', color: 'red', text: '高风险' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const riskLevel = getRiskLevel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 分析状态 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
分析状态
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">选择地块</span>
|
||||||
|
<Badge variant={state.selectedBlocks.length > 0 ? 'default' : 'secondary'}>
|
||||||
|
{state.selectedBlocks.length} / {state.landBlocks.length}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">评价因子</span>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{enabledFactorsCount} / {state.factors.length}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">分析结果</span>
|
||||||
|
<Badge variant={state.analysisResults.length > 0 ? 'default' : 'secondary'}>
|
||||||
|
{state.analysisResults.length} 个
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 权重配置 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<Sliders className="w-4 h-4" />
|
||||||
|
权重配置
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm">土壤条件</span>
|
||||||
|
<span className="text-sm font-medium">{Math.round(state.weightConfig.soil * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={state.weightConfig.soil * 100} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm">气候条件</span>
|
||||||
|
<span className="text-sm font-medium">{Math.round(state.weightConfig.climate * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={state.weightConfig.climate * 100} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm">地形条件</span>
|
||||||
|
<span className="text-sm font-medium">{Math.round(state.weightConfig.topography * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={state.weightConfig.topography * 100} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm">基础设施</span>
|
||||||
|
<span className="text-sm font-medium">{Math.round(state.weightConfig.infrastructure * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={state.weightConfig.infrastructure * 100} className="h-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t text-center">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
总权重: {Math.round(totalWeight * 100)}%
|
||||||
|
{Math.abs(totalWeight - 1) > 0.01 && (
|
||||||
|
<span className="text-red-500 ml-1">(需调整为100%)</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full mt-3"
|
||||||
|
onClick={() => dispatch({ type: 'TOGGLE_WEIGHT_DIALOG' })}
|
||||||
|
>
|
||||||
|
<Settings className="w-3 h-3 mr-2" />
|
||||||
|
调整权重
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 风险评估 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-4 h-4" />
|
||||||
|
风险评估
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">综合风险等级</span>
|
||||||
|
<Badge
|
||||||
|
variant={riskLevel.level === 'high' ? 'destructive' :
|
||||||
|
riskLevel.level === 'medium' ? 'default' : 'secondary'}
|
||||||
|
className="capitalize"
|
||||||
|
>
|
||||||
|
{riskLevel.text}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state.analysisResults.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="text-center py-2">
|
||||||
|
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
{Math.round(
|
||||||
|
state.analysisResults.reduce((sum, r) => sum + r.overallScore, 0) /
|
||||||
|
state.analysisResults.length
|
||||||
|
)}分
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">平均适宜性得分</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">优秀地块 (≥80分)</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{state.analysisResults.filter(r => r.overallScore >= 80).length} 个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">良好地块 (60-79分)</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{state.analysisResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length} 个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">需改进 (<60分)</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{state.analysisResults.filter(r => r.overallScore < 60).length} 个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 快速操作 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
快速操作
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => dispatch({ type: 'TOGGLE_FACTOR_DIALOG' })}
|
||||||
|
>
|
||||||
|
<Settings className="w-3 h-3 mr-2" />
|
||||||
|
配置评价因子
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleQuickAnalysis}
|
||||||
|
disabled={state.landBlocks.length === 0}
|
||||||
|
>
|
||||||
|
<BarChart3 className="w-3 h-3 mr-2" />
|
||||||
|
快速分析全部
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleResetFilters}
|
||||||
|
disabled={state.analysisResults.length === 0}
|
||||||
|
>
|
||||||
|
<Info className="w-3 h-3 mr-2" />
|
||||||
|
重置筛选条件
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 分析建议 */}
|
||||||
|
{state.analysisResults.length > 0 && (
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
智能建议
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="p-2 bg-blue-50 dark:bg-blue-950 rounded text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="font-medium mb-1">优先推荐作物</p>
|
||||||
|
<p className="text-xs">
|
||||||
|
{(() => {
|
||||||
|
const cropCounts = state.analysisResults.flatMap(r =>
|
||||||
|
r.recommendedCrops.slice(0, 2).map(c => c.crop.name)
|
||||||
|
);
|
||||||
|
const topCrops = [...new Set(cropCounts)].slice(0, 3);
|
||||||
|
return topCrops.join('、');
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2 bg-green-50 dark:bg-green-950 rounded text-green-800 dark:text-green-200">
|
||||||
|
<p className="font-medium mb-1">最佳种植地块</p>
|
||||||
|
<p className="text-xs">
|
||||||
|
{state.analysisResults
|
||||||
|
.sort((a, b) => b.overallScore - a.overallScore)
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(r => r.blockName)
|
||||||
|
.join('、')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Download,
|
||||||
|
Calendar,
|
||||||
|
MapPin,
|
||||||
|
BarChart3,
|
||||||
|
Star,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
|
TrendingUp,
|
||||||
|
Users
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface AnalysisReportProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnalysisReport({ state, dispatch }: AnalysisReportProps) {
|
||||||
|
const handleGenerateReport = () => {
|
||||||
|
// 生成分析报告
|
||||||
|
const reportTitle = `地块适宜性分析报告_${new Date().toLocaleDateString('zh-CN')}`;
|
||||||
|
const reportData = generateReportData();
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'GENERATE_REPORT',
|
||||||
|
payload: { title: reportTitle }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置当前报告
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CURRENT_REPORT',
|
||||||
|
payload: reportData
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateReportData = () => {
|
||||||
|
const selectedBlocks = state.landBlocks.filter(b => state.selectedBlocks.includes(b.id));
|
||||||
|
const selectedResults = state.analysisResults.filter(r => state.selectedBlocks.includes(r.blockId));
|
||||||
|
|
||||||
|
// 统计推荐作物
|
||||||
|
const cropStats = selectedResults
|
||||||
|
.flatMap(result => result.recommendedCrops.slice(0, 3))
|
||||||
|
.reduce((acc, rec) => {
|
||||||
|
const existing = acc.find(item => item.crop === rec.crop.name);
|
||||||
|
if (existing) {
|
||||||
|
existing.frequency += 1;
|
||||||
|
existing.totalScore += rec.suitabilityScore;
|
||||||
|
existing.avgScore = Math.round(existing.totalScore / existing.frequency);
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
crop: rec.crop.name,
|
||||||
|
category: rec.crop.category,
|
||||||
|
frequency: 1,
|
||||||
|
totalScore: rec.suitabilityScore,
|
||||||
|
avgScore: rec.suitabilityScore,
|
||||||
|
economicValue: rec.crop.economicValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as any[])
|
||||||
|
.sort((a, b) => b.frequency - a.frequency);
|
||||||
|
|
||||||
|
// 风险统计
|
||||||
|
const riskStats = {
|
||||||
|
low: selectedResults.filter(r => r.overallScore >= 80).length,
|
||||||
|
medium: selectedResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length,
|
||||||
|
high: selectedResults.filter(r => r.overallScore < 60).length
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
title: `地块适宜性分析报告_${new Date().toLocaleDateString('zh-CN')}`,
|
||||||
|
createdAt: new Date().toLocaleString('zh-CN'),
|
||||||
|
landBlocks: selectedBlocks,
|
||||||
|
results: selectedResults,
|
||||||
|
factors: state.factors.filter(f => f.enabled),
|
||||||
|
weightConfig: state.weightConfig,
|
||||||
|
summary: {
|
||||||
|
totalBlocks: selectedBlocks.length,
|
||||||
|
averageScore: selectedResults.length > 0
|
||||||
|
? Math.round(selectedResults.reduce((sum, r) => sum + r.overallScore, 0) / selectedResults.length)
|
||||||
|
: 0,
|
||||||
|
topRecommendedCrops: cropStats.slice(0, 5),
|
||||||
|
riskDistribution: riskStats,
|
||||||
|
totalArea: selectedBlocks.reduce((sum, b) => sum + b.area, 0),
|
||||||
|
estimatedProduction: selectedResults.reduce((sum, r) => sum + (r.estimatedYield || 0), 0),
|
||||||
|
estimatedRevenue: selectedResults.reduce((sum, r) => sum + (r.economicReturn || 0), 0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadReport = () => {
|
||||||
|
const report = state.currentReport || generateReportData();
|
||||||
|
|
||||||
|
// 生成HTML报告
|
||||||
|
const htmlContent = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${report.title}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; line-height: 1.6; }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #ccc; padding-bottom: 20px; margin-bottom: 30px; }
|
||||||
|
.section { margin-bottom: 30px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; }
|
||||||
|
.highlight { background: #e8f5e8; border-color: #4caf50; }
|
||||||
|
.warning { background: #fff3cd; border-color: #ffc107; }
|
||||||
|
.danger { background: #f8d7da; border-color: #dc3545; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||||
|
th { background-color: #f2f2f2; }
|
||||||
|
.badge { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
|
||||||
|
.badge-success { background: #28a745; color: white; }
|
||||||
|
.badge-warning { background: #ffc107; color: black; }
|
||||||
|
.badge-danger { background: #dc3545; color: white; }
|
||||||
|
.progress { width: 100%; height: 20px; background: #e9ecef; border-radius: 10px; overflow: hidden; }
|
||||||
|
.progress-bar { height: 100%; background: #28a745; transition: width 0.3s; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>${report.title}</h1>
|
||||||
|
<p>生成时间: ${report.createdAt}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>执行摘要</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card highlight">
|
||||||
|
<h3>分析概览</h3>
|
||||||
|
<p><strong>地块数量:</strong> ${report.summary.totalBlocks} 个</p>
|
||||||
|
<p><strong>总面积:</strong> ${report.summary.totalArea} 亩</p>
|
||||||
|
<p><strong>平均得分:</strong> ${report.summary.averageScore} 分</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>经济效益</h3>
|
||||||
|
<p><strong>预估产量:</strong> ${report.summary.estimatedProduction.toLocaleString()} kg</p>
|
||||||
|
<p><strong>预估收益:</strong> ¥${report.summary.estimatedRevenue.toLocaleString()}</p>
|
||||||
|
<p><strong>亩均收益:</strong> ¥${Math.round(report.summary.estimatedRevenue / report.summary.totalArea)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>风险分布</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card highlight">
|
||||||
|
<h3>低风险地块</h3>
|
||||||
|
<p class="badge badge-success">${report.summary.riskDistribution.low} 个</p>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: ${(report.summary.riskDistribution.low / report.summary.totalBlocks * 100)}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card warning">
|
||||||
|
<h3>中等风险地块</h3>
|
||||||
|
<p class="badge badge-warning">${report.summary.riskDistribution.medium} 个</p>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: ${(report.summary.riskDistribution.medium / report.summary.totalBlocks * 100)}%; background: #ffc107;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card danger">
|
||||||
|
<h3>高风险地块</h3>
|
||||||
|
<p class="badge badge-danger">${report.summary.riskDistribution.high} 个</p>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: ${(report.summary.riskDistribution.high / report.summary.totalBlocks * 100)}%; background: #dc3545;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>推荐作物排名</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>排名</th>
|
||||||
|
<th>作物名称</th>
|
||||||
|
<th>类别</th>
|
||||||
|
<th>推荐频次</th>
|
||||||
|
<th>平均适宜性</th>
|
||||||
|
<th>经济效益</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${report.summary.topRecommendedCrops.map((crop, index) => `
|
||||||
|
<tr>
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td><strong>${crop.crop}</strong></td>
|
||||||
|
<td>${crop.category}</td>
|
||||||
|
<td>${crop.frequency} 次</td>
|
||||||
|
<td>${crop.avgScore} 分</td>
|
||||||
|
<td>¥${crop.economicValue}/亩</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>详细地块分析</h2>
|
||||||
|
${report.results.map(result => {
|
||||||
|
const block = report.landBlocks.find(b => b.id === result.blockId);
|
||||||
|
return `
|
||||||
|
<div class="card" style="margin-bottom: 20px;">
|
||||||
|
<h3>${result.blockName}</h3>
|
||||||
|
<p><strong>地块信息:</strong> ${block?.area} 亩 | ${block?.soilType} | ${block?.irrigation}</p>
|
||||||
|
<p><strong>综合得分:</strong> ${result.overallScore} 分</p>
|
||||||
|
<p><strong>推荐作物:</strong> ${result.recommendedCrops.slice(0, 3).map(c => c.crop.name).join('、')}</p>
|
||||||
|
<p><strong>风险因素:</strong> ${result.riskFactors.join(';') || '无'}</p>
|
||||||
|
<p><strong>改进建议:</strong> ${result.improvementSuggestions.join(';')}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>分析说明</h2>
|
||||||
|
<div class="card">
|
||||||
|
<h3>评价方法</h3>
|
||||||
|
<p>本次分析采用多因子综合评价法,考虑土壤条件、气候条件、地形条件和基础设施四个维度。</p>
|
||||||
|
<p><strong>权重配置:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>土壤条件: ${Math.round(report.weightConfig.soil * 100)}%</li>
|
||||||
|
<li>气候条件: ${Math.round(report.weightConfig.climate * 100)}%</li>
|
||||||
|
<li>地形条件: ${Math.round(report.weightConfig.topography * 100)}%</li>
|
||||||
|
<li>基础设施: ${Math.round(report.weightConfig.infrastructure * 100)}%</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="text-align: center; color: #666; font-size: 12px;">
|
||||||
|
<p>本报告由智慧农业生产管理系统自动生成</p>
|
||||||
|
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 下载HTML文件
|
||||||
|
const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `${state.currentReport?.title || '分析报告'}.html`;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentReport = state.currentReport || generateReportData();
|
||||||
|
|
||||||
|
if (state.analysisResults.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<FileText className="w-16 h-16 mx-auto mb-4 text-muted-foreground opacity-50" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">暂无分析数据</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
请先运行分析生成结果
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => dispatch({ type: 'SET_ACTIVE_TAB', payload: 'overview' })}>
|
||||||
|
开始分析
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 报告操作 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
<h3 className="text-lg font-medium">分析报告</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={handleGenerateReport}>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
生成报告
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDownloadReport}>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
下载报告
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 报告概览 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Calendar className="w-5 h-5 text-blue-500" />
|
||||||
|
<h4 className="font-medium">{currentReport.title}</h4>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{currentReport.createdAt}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{currentReport.summary.totalBlocks}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">分析地块</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{currentReport.summary.averageScore}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">平均得分</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">
|
||||||
|
{currentReport.summary.totalArea}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">总面积(亩)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-orange-50 dark:bg-orange-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-orange-600">
|
||||||
|
{Math.round(currentReport.summary.estimatedRevenue / 1000)}K
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">预估收益</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 推荐作物排名 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<Star className="w-4 h-4 text-yellow-500" />
|
||||||
|
推荐作物排名
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{currentReport.summary.topRecommendedCrops.map((crop, index) => (
|
||||||
|
<div key={crop.crop} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white font-bold ${
|
||||||
|
index === 0 ? 'bg-yellow-500' :
|
||||||
|
index === 1 ? 'bg-gray-400' :
|
||||||
|
index === 2 ? 'bg-orange-600' :
|
||||||
|
'bg-blue-500'
|
||||||
|
}`}>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{crop.crop}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{crop.category}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm text-muted-foreground">推荐频次</div>
|
||||||
|
<div className="font-medium">{crop.frequency} 次</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm text-muted-foreground">适宜性</div>
|
||||||
|
<div className="font-medium">{crop.avgScore} 分</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm text-muted-foreground">经济效益</div>
|
||||||
|
<div className="font-medium">¥{crop.economicValue}/亩</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 风险分析 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-4 h-4 text-orange-500" />
|
||||||
|
风险分析
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium text-green-900 dark:text-green-100">低风险地块</span>
|
||||||
|
<Badge className="bg-green-600">{currentReport.summary.riskDistribution.low} 个</Badge>
|
||||||
|
</div>
|
||||||
|
<Progress value={(currentReport.summary.riskDistribution.low / currentReport.summary.totalBlocks) * 100} className="h-2" />
|
||||||
|
<p className="text-sm text-green-800 dark:text-green-200 mt-2">
|
||||||
|
综合得分≥80分,适宜种植推荐作物
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium text-yellow-900 dark:text-yellow-100">中等风险地块</span>
|
||||||
|
<Badge className="bg-yellow-600">{currentReport.summary.riskDistribution.medium} 个</Badge>
|
||||||
|
</div>
|
||||||
|
<Progress value={(currentReport.summary.riskDistribution.medium / currentReport.summary.totalBlocks) * 100} className="h-2" />
|
||||||
|
<p className="text-sm text-yellow-800 dark:text-yellow-200 mt-2">
|
||||||
|
综合得分60-79分,需要一定改进措施
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium text-red-900 dark:text-red-100">高风险地块</span>
|
||||||
|
<Badge className="bg-red-600">{currentReport.summary.riskDistribution.high} 个</Badge>
|
||||||
|
</div>
|
||||||
|
<Progress value={(currentReport.summary.riskDistribution.high / currentReport.summary.totalBlocks) * 100} className="h-2" />
|
||||||
|
<p className="text-sm text-red-800 dark:text-red-200 mt-2">
|
||||||
|
综合得分<60分,需要重点改进
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 详细分析结果 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-4 h-4 text-blue-500" />
|
||||||
|
详细分析结果
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentReport.results.map(result => {
|
||||||
|
const block = currentReport.landBlocks.find(b => b.id === result.blockId);
|
||||||
|
return (
|
||||||
|
<div key={result.blockId} className="p-4 border rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h5 className="font-medium flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4 text-blue-500" />
|
||||||
|
{result.blockName}
|
||||||
|
</h5>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{result.overallScore} 分
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm mb-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">面积:</span>
|
||||||
|
<span className="ml-1">{block?.area} 亩</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">土壤:</span>
|
||||||
|
<span className="ml-1">{block?.soilType}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">灌溉:</span>
|
||||||
|
<span className="ml-1">{block?.irrigation}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">预估收益:</span>
|
||||||
|
<span className="ml-1">¥{result.economicReturn?.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-green-600">推荐作物:</span>
|
||||||
|
<div className="mt-1">
|
||||||
|
{result.recommendedCrops.slice(0, 3).map((rec, index) => (
|
||||||
|
<Badge key={index} variant="outline" className="mr-2 mb-1">
|
||||||
|
{rec.crop.name} ({rec.suitabilityScore}分)
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-orange-600">风险因素:</span>
|
||||||
|
<div className="mt-1 text-muted-foreground">
|
||||||
|
{result.riskFactors.length > 0 ? result.riskFactors.join(';') : '无'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 报告说明 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="font-medium mb-2">报告说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• 本报告基于多因子综合评价模型,结合土壤、气候、地形、基础设施等条件进行分析</li>
|
||||||
|
<li>• 推荐作物基于各地区的农业实践数据和科学研究成果</li>
|
||||||
|
<li>• 经济效益估算仅供参考,实际收益受市场价格、管理水平等多种因素影响</li>
|
||||||
|
<li>• 建议结合实地调研和专家意见,制定最终的种植决策方案</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
CheckCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
Star,
|
||||||
|
TrendingUp,
|
||||||
|
BarChart3,
|
||||||
|
Filter,
|
||||||
|
ArrowUpDown
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface AnalysisResultsProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnalysisResults({ state, dispatch }: AnalysisResultsProps) {
|
||||||
|
const getScoreColor = (score: number) => {
|
||||||
|
if (score >= 80) return 'text-green-600';
|
||||||
|
if (score >= 60) return 'text-yellow-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGradeLabel = (score: number) => {
|
||||||
|
if (score >= 80) return '高度适宜';
|
||||||
|
if (score >= 60) return '一般适宜';
|
||||||
|
return '不适宜';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGradeColor = (score: number) => {
|
||||||
|
if (score >= 80) return 'bg-green-100 text-green-800';
|
||||||
|
if (score >= 60) return 'bg-yellow-100 text-yellow-800';
|
||||||
|
return 'bg-red-100 text-red-800';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 筛选结果
|
||||||
|
const filteredResults = state.analysisResults.filter(result => {
|
||||||
|
if (state.filters.scoreRange) {
|
||||||
|
const [min, max] = state.filters.scoreRange;
|
||||||
|
if (result.overallScore < min || result.overallScore > max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 排序结果
|
||||||
|
const sortedResults = [...filteredResults].sort((a, b) => {
|
||||||
|
let aValue: number, bValue: number;
|
||||||
|
|
||||||
|
switch (state.sortBy) {
|
||||||
|
case 'name':
|
||||||
|
aValue = a.blockName.charCodeAt(0);
|
||||||
|
bValue = b.blockName.charCodeAt(0);
|
||||||
|
break;
|
||||||
|
case 'area':
|
||||||
|
aValue = a.estimatedYield || 0;
|
||||||
|
bValue = b.estimatedYield || 0;
|
||||||
|
break;
|
||||||
|
case 'score':
|
||||||
|
default:
|
||||||
|
aValue = a.overallScore;
|
||||||
|
bValue = b.overallScore;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.sortOrder === 'asc' ? aValue - bValue : bValue - aValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportResults = () => {
|
||||||
|
// 导出分析结果为CSV
|
||||||
|
const headers = [
|
||||||
|
'地块名称', '综合得分', '适宜性等级', '土壤得分', '气候得分', '地形得分', '基础设施得分',
|
||||||
|
'推荐作物', '预估产量(kg)', '经济效益(元)', '风险因素', '改进建议'
|
||||||
|
];
|
||||||
|
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...sortedResults.map(result => {
|
||||||
|
const topCrop = result.recommendedCrops[0];
|
||||||
|
const riskFactors = result.riskFactors.join(';') || '无';
|
||||||
|
const improvements = result.improvementSuggestions.join(';') || '无';
|
||||||
|
|
||||||
|
return [
|
||||||
|
result.blockName,
|
||||||
|
result.overallScore,
|
||||||
|
getGradeLabel(result.overallScore),
|
||||||
|
Math.round(result.soilScore),
|
||||||
|
Math.round(result.climateScore),
|
||||||
|
Math.round(result.topographyScore),
|
||||||
|
Math.round(result.infrastructureScore),
|
||||||
|
topCrop ? topCrop.crop.name : '无',
|
||||||
|
result.estimatedYield?.toLocaleString() || '',
|
||||||
|
result.economicReturn?.toLocaleString() || '',
|
||||||
|
riskFactors,
|
||||||
|
improvements
|
||||||
|
].join(',');
|
||||||
|
})
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `地块适宜性评价结果_${new Date().toLocaleDateString('zh-CN')}.csv`;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.analysisResults.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Star className="w-5 h-5 text-yellow-500" />
|
||||||
|
地块适宜性评价结果
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
共 {sortedResults.length} 个地块的评价结果
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select value={state.sortBy} onValueChange={(value: 'score' | 'name' | 'area') =>
|
||||||
|
dispatch({ type: 'SET_SORT_BY', payload: value })}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue placeholder="排序方式" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="score">综合得分</SelectItem>
|
||||||
|
<SelectItem value="name">地块名称</SelectItem>
|
||||||
|
<SelectItem value="area">预估产量</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: 'SET_SORT_ORDER',
|
||||||
|
payload: state.sortOrder === 'asc' ? 'desc' : 'asc'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ArrowUpDown className="w-4 h-4 mr-2" />
|
||||||
|
{state.sortOrder === 'asc' ? '升序' : '降序'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={exportResults}>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出结果
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计摘要 */}
|
||||||
|
<div className="mb-6 grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{sortedResults.filter(r => r.overallScore >= 80).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">高度适宜</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-yellow-600">
|
||||||
|
{sortedResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">一般适宜</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-red-50 dark:bg-red-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-red-600">
|
||||||
|
{sortedResults.filter(r => r.overallScore < 60).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">不适宜</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{Math.round(
|
||||||
|
sortedResults.reduce((sum, r) => sum + r.overallScore, 0) /
|
||||||
|
sortedResults.length
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">平均得分</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 结果表格 */}
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<TableHead className="w-[120px]">地块名称</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-center">综合得分</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-center">适宜性等级</TableHead>
|
||||||
|
<TableHead className="w-[80px] text-center">土壤得分</TableHead>
|
||||||
|
<TableHead className="w-[80px] text-center">气候得分</TableHead>
|
||||||
|
<TableHead className="w-[80px] text-center">地形得分</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-center">基础设施得分</TableHead>
|
||||||
|
<TableHead className="w-[120px]">推荐作物</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-center">预估产量</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-center">经济效益</TableHead>
|
||||||
|
<TableHead className="w-[200px]">风险因素</TableHead>
|
||||||
|
<TableHead className="w-[200px]">改进建议</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sortedResults.map((result, index) => {
|
||||||
|
const topCrop = result.recommendedCrops[0];
|
||||||
|
return (
|
||||||
|
<TableRow key={result.blockId} className="hover:bg-gray-50 dark:hover:bg-gray-900">
|
||||||
|
<TableCell className="font-medium">{result.blockName}</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className={`text-lg font-bold ${getScoreColor(result.overallScore)}`}>
|
||||||
|
{result.overallScore}
|
||||||
|
</div>
|
||||||
|
<Progress value={result.overallScore} className="w-16 h-2 mt-1" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<Badge className={getGradeColor(result.overallScore)}>
|
||||||
|
{getGradeLabel(result.overallScore)}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="text-blue-600 font-medium">
|
||||||
|
{Math.round(result.soilScore)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="text-green-600 font-medium">
|
||||||
|
{Math.round(result.climateScore)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="text-orange-600 font-medium">
|
||||||
|
{Math.round(result.topographyScore)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="text-purple-600 font-medium">
|
||||||
|
{Math.round(result.infrastructureScore)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{topCrop && (
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{topCrop.crop.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{topCrop.crop.category}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="text-sm">
|
||||||
|
<div>{result.estimatedYield?.toLocaleString()} kg</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="text-sm">
|
||||||
|
<div>¥{result.economicReturn?.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm text-red-600">
|
||||||
|
{result.riskFactors.length > 0 ? (
|
||||||
|
<div className="max-w-[180px]">
|
||||||
|
{result.riskFactors.join('、')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-green-600">无明显风险</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm text-blue-600">
|
||||||
|
<div className="max-w-[180px]">
|
||||||
|
{result.improvementSuggestions.join('、')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 详细统计信息 */}
|
||||||
|
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<BarChart3 className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<h4 className="font-medium mb-2">统计分析</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-blue-700 dark:text-blue-300 mb-1">得分分布</h5>
|
||||||
|
<ul className="text-xs space-y-1">
|
||||||
|
<li>• 高度适宜 (80-100分): {sortedResults.filter(r => r.overallScore >= 80).length} 个地块</li>
|
||||||
|
<li>• 一般适宜 (60-79分): {sortedResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length} 个地块</li>
|
||||||
|
<li>• 不适宜 (0-59分): {sortedResults.filter(r => r.overallScore < 60).length} 个地块</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-blue-700 dark:text-blue-300 mb-1">经济效益</h5>
|
||||||
|
<ul className="text-xs space-y-1">
|
||||||
|
<li>• 总预估产量: {sortedResults.reduce((sum, r) => sum + (r.estimatedYield || 0), 0).toLocaleString()} kg</li>
|
||||||
|
<li>• 总经济效益: ¥{sortedResults.reduce((sum, r) => sum + (r.economicReturn || 0), 0).toLocaleString()}</li>
|
||||||
|
<li>• 平均亩产: {Math.round(sortedResults.reduce((sum, r) => sum + (r.estimatedYield || 0), 0) / sortedResults.length).toLocaleString()} kg</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-blue-700 dark:text-blue-300 mb-1">因子表现</h5>
|
||||||
|
<ul className="text-xs space-y-1">
|
||||||
|
<li>• 平均土壤得分: {Math.round(sortedResults.reduce((sum, r) => sum + r.soilScore, 0) / sortedResults.length)} 分</li>
|
||||||
|
<li>• 平均气候得分: {Math.round(sortedResults.reduce((sum, r) => sum + r.climateScore, 0) / sortedResults.length)} 分</li>
|
||||||
|
<li>• 平均地形得分: {Math.round(sortedResults.reduce((sum, r) => sum + r.topographyScore, 0) / sortedResults.length)} 分</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import {
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
Square,
|
||||||
|
RotateCcw,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Loader2,
|
||||||
|
Activity,
|
||||||
|
Database,
|
||||||
|
TrendingUp
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
import BatchAnalysisManager from './batchAnalysisService';
|
||||||
|
|
||||||
|
interface BatchAnalysisPanelProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BatchAnalysisPanel({ state, dispatch }: BatchAnalysisPanelProps) {
|
||||||
|
const manager = new BatchAnalysisManager(dispatch);
|
||||||
|
|
||||||
|
const handleStartBatchAnalysis = async () => {
|
||||||
|
if (state.selectedBlocks.length === 0) {
|
||||||
|
alert('请先选择要分析的地块');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedBlocksData = state.landBlocks.filter(block =>
|
||||||
|
state.selectedBlocks.includes(block.id)
|
||||||
|
);
|
||||||
|
const enabledFactors = state.factors.filter(factor => factor.enabled);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await manager.startBatchAnalysis(
|
||||||
|
`批量分析_${new Date().toLocaleDateString('zh-CN')}`,
|
||||||
|
selectedBlocksData,
|
||||||
|
enabledFactors,
|
||||||
|
state.weightConfig
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动批量分析失败:', error);
|
||||||
|
alert('启动批量分析失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePauseAnalysis = () => {
|
||||||
|
manager.pauseAnalysis();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResumeAnalysis = () => {
|
||||||
|
manager.resumeAnalysis();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelAnalysis = () => {
|
||||||
|
if (confirm('确定要取消当前的分析任务吗?')) {
|
||||||
|
manager.cancelAnalysis();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return <Clock className="w-4 h-4 text-yellow-500" />;
|
||||||
|
case 'running':
|
||||||
|
return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
|
||||||
|
case 'paused':
|
||||||
|
return <Pause className="w-4 h-4 text-orange-500" />;
|
||||||
|
case 'completed':
|
||||||
|
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||||
|
case 'failed':
|
||||||
|
return <XCircle className="w-4 h-4 text-red-500" />;
|
||||||
|
default:
|
||||||
|
return <Activity className="w-4 h-4 text-gray-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return '等待开始';
|
||||||
|
case 'running':
|
||||||
|
return '正在分析';
|
||||||
|
case 'paused':
|
||||||
|
return '已暂停';
|
||||||
|
case 'completed':
|
||||||
|
return '已完成';
|
||||||
|
case 'failed':
|
||||||
|
return '分析失败';
|
||||||
|
default:
|
||||||
|
return '未知状态';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-yellow-50 border-yellow-200 dark:bg-yellow-950 dark:border-yellow-800';
|
||||||
|
case 'running':
|
||||||
|
return 'bg-blue-50 border-blue-200 dark:bg-blue-950 dark:border-blue-800';
|
||||||
|
case 'paused':
|
||||||
|
return 'bg-orange-50 border-orange-200 dark:bg-orange-950 dark:border-orange-800';
|
||||||
|
case 'completed':
|
||||||
|
return 'bg-green-50 border-green-200 dark:bg-green-950 dark:border-green-800';
|
||||||
|
case 'failed':
|
||||||
|
return 'bg-red-50 border-red-200 dark:bg-red-950 dark:border-red-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50 border-gray-200 dark:bg-gray-950 dark:border-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentTask = state.currentTask;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 批量分析控制面板 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Database className="w-5 h-5" />
|
||||||
|
批量评价地块
|
||||||
|
</h3>
|
||||||
|
<Badge variant="outline">
|
||||||
|
已选择 {state.selectedBlocks.length} / {state.landBlocks.length} 个地块
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 任务状态显示 */}
|
||||||
|
{currentTask && (
|
||||||
|
<div className={`p-4 rounded-lg border mb-6 ${getStatusColor(currentTask.status)}`}>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(currentTask.status)}
|
||||||
|
<h4 className="font-medium">{currentTask.name}</h4>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{getStatusText(currentTask.status)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm mb-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">创建时间:</span>
|
||||||
|
<div className="font-medium">{currentTask.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">总地块:</span>
|
||||||
|
<div className="font-medium">{currentTask.totalBlocks} 个</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">已处理:</span>
|
||||||
|
<div className="font-medium">{currentTask.processedBlocks} 个</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">成功率:</span>
|
||||||
|
<div className="font-medium text-green-600">
|
||||||
|
{currentTask.totalBlocks > 0
|
||||||
|
? Math.round((currentTask.successCount / currentTask.processedBlocks) * 100)
|
||||||
|
: 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>分析进度</span>
|
||||||
|
<span>{currentTask.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={currentTask.progress} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 当前处理的地块 */}
|
||||||
|
{currentTask.currentProcessingBlock && (
|
||||||
|
<div className="mt-3 text-sm">
|
||||||
|
<span className="text-muted-foreground">正在处理: </span>
|
||||||
|
<span className="font-medium">{currentTask.currentProcessingBlock}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 错误信息 */}
|
||||||
|
{currentTask.errorMessage && (
|
||||||
|
<div className="mt-3 text-sm text-red-600">
|
||||||
|
错误: {currentTask.errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 控制按钮 */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{!state.isBatchAnalyzing && !currentTask && (
|
||||||
|
<Button
|
||||||
|
onClick={handleStartBatchAnalysis}
|
||||||
|
disabled={state.selectedBlocks.length === 0}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
开始批量分析
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.isBatchAnalyzing && currentTask && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handlePauseAnalysis}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Pause className="w-4 h-4" />
|
||||||
|
暂停
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancelAnalysis}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4" />
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentTask && currentTask.status === 'paused' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={handleResumeAnalysis}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
继续
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancelAnalysis}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4" />
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentTask && currentTask.status === 'completed' && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleStartBatchAnalysis}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
重新分析
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 分析配置信息 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
分析配置
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h5 className="text-sm font-medium mb-2">权重配置</h5>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>土壤条件:</span>
|
||||||
|
<span>{Math.round(state.weightConfig.soil * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>气候条件:</span>
|
||||||
|
<span>{Math.round(state.weightConfig.climate * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>地形条件:</span>
|
||||||
|
<span>{Math.round(state.weightConfig.topography * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>基础设施:</span>
|
||||||
|
<span>{Math.round(state.weightConfig.infrastructure * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5 className="text-sm font-medium mb-2">评价因子</h5>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>启用因子:</span>
|
||||||
|
<span>{state.factors.filter(f => f.enabled).length} / {state.factors.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>土壤因子:</span>
|
||||||
|
<span>{state.factors.filter(f => f.enabled && f.category === 'soil').length} 个</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>气候因子:</span>
|
||||||
|
<span>{state.factors.filter(f => f.enabled && f.category === 'climate').length} 个</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>地形因子:</span>
|
||||||
|
<span>{state.factors.filter(f => f.enabled && f.category === 'topography').length} 个</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 地块分析状态概览 */}
|
||||||
|
{state.landBlocks.some(block => block.analysisStatus) && (
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<Activity className="w-4 h-4" />
|
||||||
|
地块分析状态
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{state.landBlocks.filter(b => b.analysisStatus === 'pending').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">等待分析</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-orange-600">
|
||||||
|
{state.landBlocks.filter(b => b.analysisStatus === 'analyzing').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">正在分析</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{state.landBlocks.filter(b => b.analysisStatus === 'completed').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">分析完成</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-red-600">
|
||||||
|
{state.landBlocks.filter(b => b.analysisStatus === 'failed').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">分析失败</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 使用说明 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Database className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="font-medium mb-2">批量评价说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>批量处理</strong>: 选择多个地块,系统将自动循环调用空间分析服务</li>
|
||||||
|
<li>• <strong>因子读取</strong>: 自动读取土壤、气候、地形、基础设施等各项因子数据</li>
|
||||||
|
<li>• <strong>加权计算</strong>: 根据配置的权重和因子进行综合评分计算</li>
|
||||||
|
<li>• <strong>适宜性指数</strong>: 生成0-100的适宜性指数并自动更新到数据库</li>
|
||||||
|
<li>• <strong>进度监控</strong>: 实时显示分析进度,支持暂停、继续、取消操作</li>
|
||||||
|
<li>• <strong>结果保存</strong>: 分析完成后自动保存结果,可查看详细报告</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Database,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertTriangle,
|
||||||
|
Target,
|
||||||
|
Droplet,
|
||||||
|
Cloud,
|
||||||
|
Sun,
|
||||||
|
ThermometerSun,
|
||||||
|
Zap,
|
||||||
|
RefreshCw,
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
Square
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction, AnalysisResult } from './spatialAnalysisReducer';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import BatchAnalysisManager from './batchAnalysisService';
|
||||||
|
|
||||||
|
interface BatchEvaluationPanelProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BatchEvaluationPanel({ state, dispatch }: BatchEvaluationPanelProps) {
|
||||||
|
const [batchManager, setBatchManager] = useState<BatchAnalysisManager | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBatchManager(new BatchAnalysisManager(dispatch));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const totalWeight = Math.round(
|
||||||
|
(state.weightConfig.soil + state.weightConfig.climate +
|
||||||
|
state.weightConfig.topography + state.weightConfig.infrastructure) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStartBatchAnalysis = async () => {
|
||||||
|
if (totalWeight !== 100) {
|
||||||
|
toast.error(`权重总和必须为100%才能进行批量分析(当前:${totalWeight}%)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledFactors = state.factors.filter(f => f.enabled);
|
||||||
|
if (enabledFactors.length < 3) {
|
||||||
|
toast.error('至少需要启用3个评价因子才能进行批量分析');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!batchManager) {
|
||||||
|
toast.error('批量分析服务未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建批量分析任务
|
||||||
|
const taskName = `地块适宜性批量分析_${new Date().toLocaleString('zh-CN')}`;
|
||||||
|
|
||||||
|
// 固定分析68个地块
|
||||||
|
const totalFields = 68;
|
||||||
|
const blockIds = Array.from({ length: totalFields }, (_, i) => `field-${i + 1}`);
|
||||||
|
|
||||||
|
// 开始批量分析
|
||||||
|
await batchManager.startBatchAnalysis(taskName, enabledFactors, state.weightConfig);
|
||||||
|
|
||||||
|
toast.success('批量分析任务已启动,正在处理地块数据...');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动批量分析失败:', error);
|
||||||
|
toast.error('启动批量分析失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePauseAnalysis = () => {
|
||||||
|
if (batchManager && state.currentTask) {
|
||||||
|
batchManager.pauseAnalysis();
|
||||||
|
toast.info('批量分析已暂停');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResumeAnalysis = () => {
|
||||||
|
if (batchManager && state.currentTask) {
|
||||||
|
batchManager.resumeAnalysis();
|
||||||
|
toast.info('批量分析已恢复');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelAnalysis = () => {
|
||||||
|
if (batchManager && state.currentTask) {
|
||||||
|
batchManager.cancelAnalysis();
|
||||||
|
toast.info('批量分析已取消');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentTask = state.currentTask;
|
||||||
|
|
||||||
|
// 计算统计结果
|
||||||
|
const resultsForStats = state.analysisResults;
|
||||||
|
const highSuitability = resultsForStats.filter(r => r.overallScore >= 80).length;
|
||||||
|
const mediumSuitability = resultsForStats.filter(r => r.overallScore >= 60 && r.overallScore < 80).length;
|
||||||
|
const lowSuitability = resultsForStats.filter(r => r.overallScore < 60).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 主要批量分析控制面板 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Database className="w-5 h-5" />
|
||||||
|
批量评价地块
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline">
|
||||||
|
68 个地块
|
||||||
|
</Badge>
|
||||||
|
{currentTask && (
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
currentTask.status === 'completed' ? 'default' :
|
||||||
|
currentTask.status === 'running' ? 'secondary' :
|
||||||
|
currentTask.status === 'failed' ? 'destructive' : 'outline'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{currentTask.status === 'pending' && '等待中'}
|
||||||
|
{currentTask.status === 'running' && '分析中'}
|
||||||
|
{currentTask.status === 'completed' && '已完成'}
|
||||||
|
{currentTask.status === 'failed' && '失败'}
|
||||||
|
{currentTask.status === 'paused' && '已暂停'}
|
||||||
|
{currentTask.status === 'cancelled' && '已取消'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 控制按钮区域 */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
{!currentTask || currentTask.status === 'completed' || currentTask.status === 'failed' || currentTask.status === 'cancelled' ? (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={handleStartBatchAnalysis}
|
||||||
|
disabled={state.isBatchAnalyzing || totalWeight !== 100}
|
||||||
|
>
|
||||||
|
<Play className="w-5 h-5 mr-2" />
|
||||||
|
开始批量分析
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{currentTask.status === 'running' && (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handlePauseAnalysis}
|
||||||
|
>
|
||||||
|
<Pause className="w-5 h-5 mr-2" />
|
||||||
|
暂停分析
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{currentTask.status === 'paused' && (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={handleResumeAnalysis}
|
||||||
|
>
|
||||||
|
<Play className="w-5 h-5 mr-2" />
|
||||||
|
恢复分析
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleCancelAnalysis}
|
||||||
|
>
|
||||||
|
<Square className="w-5 h-5 mr-2" />
|
||||||
|
取消分析
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重和因子验证警告 */}
|
||||||
|
{totalWeight !== 100 && (
|
||||||
|
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<p className="text-sm text-yellow-800">
|
||||||
|
⚠️ 权重总和必须为100%才能进行批量分析(当前:{totalWeight}%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.factors.filter(f => f.enabled).length < 3 && (
|
||||||
|
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<p className="text-sm text-yellow-800">
|
||||||
|
⚠️ 至少需要启用3个评价因子才能进行批量分析(当前:{state.factors.filter(f => f.enabled).length}个)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分析进行中的状态显示 */}
|
||||||
|
{currentTask && (currentTask.status === 'running' || currentTask.status === 'paused') && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">分析进度</span>
|
||||||
|
<span className="font-medium">{currentTask.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={currentTask.progress} className="h-3" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="p-3 bg-blue-50 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">当前处理</p>
|
||||||
|
<p className="mt-1 text-blue-900">{currentTask.currentProcessingBlock || '准备中...'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-blue-50 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">处理进度</p>
|
||||||
|
<p className="mt-1 text-blue-900">
|
||||||
|
{currentTask.processedBlocks} / {currentTask.totalBlocks} 地块
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-green-50 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">成功数量</p>
|
||||||
|
<p className="mt-1 text-green-900">{currentTask.successCount}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-red-50 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">失败数量</p>
|
||||||
|
<p className="mt-1 text-red-900">{currentTask.failedCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
|
<p>✓ 正在从空间分析服务读取因子数据...</p>
|
||||||
|
<p>✓ 正在计算各因子得分(pH、有机质、土层厚度、全氮、全磷、全钾、排水性)...</p>
|
||||||
|
<p>✓ 正在进行加权汇总计算...</p>
|
||||||
|
<p>✓ 正在生成适宜性指数并更新到数据库...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 任务状态信息 */}
|
||||||
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">任务名称:</span>
|
||||||
|
<span className="font-medium">{currentTask.name}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">创建时间:</span>
|
||||||
|
<span className="font-medium">{currentTask.createdAt}</span>
|
||||||
|
</div>
|
||||||
|
{currentTask.startedAt && (
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">开始时间:</span>
|
||||||
|
<span className="font-medium">{currentTask.startedAt}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">启用因子:</span>
|
||||||
|
<span className="font-medium">{state.factors.filter(f => f.enabled).length}个</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分析完成状态 */}
|
||||||
|
{currentTask && currentTask.status === 'completed' && (
|
||||||
|
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<p className="text-sm text-green-800">
|
||||||
|
✓ 批量分析已完成!已为 {currentTask.totalBlocks} 个地块生成适宜性评价结果并更新到数据库
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 text-xs text-green-700">
|
||||||
|
<div>完成时间:{currentTask.completedAt}</div>
|
||||||
|
<div>成功分析:{currentTask.successCount} 个地块</div>
|
||||||
|
{currentTask.failedCount > 0 && (
|
||||||
|
<div>分析失败:{currentTask.failedCount} 个地块</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分析失败状态 */}
|
||||||
|
{currentTask && currentTask.status === 'failed' && (
|
||||||
|
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-sm text-red-800">
|
||||||
|
✗ 批量分析失败:{currentTask.errorMessage || '未知错误'}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 text-xs text-red-700">
|
||||||
|
<div>已处理:{currentTask.processedBlocks} 个地块</div>
|
||||||
|
<div>成功:{currentTask.successCount} 个地块</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 批量分析结果统计 */}
|
||||||
|
{resultsForStats.length > 0 && (
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<Card className="p-6 bg-gradient-to-br from-green-50 to-green-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">高度适宜</p>
|
||||||
|
<p className="mt-2 text-3xl text-green-600">{highSuitability}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
地块数量 ({Math.round((highSuitability / resultsForStats.length) * 100)}%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CheckCircle2 className="w-12 h-12 text-green-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-gradient-to-br from-yellow-50 to-yellow-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">一般适宜</p>
|
||||||
|
<p className="mt-2 text-3xl text-yellow-600">{mediumSuitability}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
地块数量 ({Math.round((mediumSuitability / resultsForStats.length) * 100)}%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AlertTriangle className="w-12 h-12 text-yellow-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-gradient-to-br from-red-50 to-red-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">不适宜</p>
|
||||||
|
<p className="mt-2 text-3xl text-red-600">{lowSuitability}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
地块数量 ({Math.round((lowSuitability / resultsForStats.length) * 100)}%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AlertTriangle className="w-12 h-12 text-red-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分析说明卡片 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4 text-yellow-500" />
|
||||||
|
空间分析服务说明
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Target className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h5 className="font-medium text-sm">土壤因子</h5>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
pH值、有机质、土层厚度、全氮、全磷、全钾、排水性
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Droplet className="w-6 h-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<h5 className="font-medium text-sm">水分因子</h5>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
地下水位、灌溉保证率、排水能力、土壤含水量
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Sun className="w-6 h-6 text-yellow-600" />
|
||||||
|
</div>
|
||||||
|
<h5 className="font-medium text-sm">气候因子</h5>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
年均温度、年降雨量、日照时数、无霜期、积温
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<ThermometerSun className="w-6 h-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<h5 className="font-medium text-sm">地形因子</h5>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
海拔高度、坡度、坡向、地貌类型、侵蚀程度
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Database className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="font-medium mb-2">批量评价流程:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>数据读取</strong>: 自动从空间分析服务读取68个地块的多维度因子数据</li>
|
||||||
|
<li>• <strong>因子评分</strong>: 根据最优值范围计算每个因子的得分(0-100分)</li>
|
||||||
|
<li>• <strong>加权汇总</strong>: 按照配置的权重计算综合适宜性指数</li>
|
||||||
|
<li>• <strong>等级划分</strong>: 根据综合得分确定适宜性等级(高度适宜/一般适宜/不适宜)</li>
|
||||||
|
<li>• <strong>数据库更新</strong>: 将评价结果和适宜性指数自动更新到数据库</li>
|
||||||
|
<li>• <strong>结果统计</strong>: 生成详细的统计分析和可视化报告</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Brain,
|
||||||
|
Settings,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Target,
|
||||||
|
Droplet,
|
||||||
|
Sun,
|
||||||
|
Mountain,
|
||||||
|
Building,
|
||||||
|
Info
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface EvaluationModelProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EvaluationModel({ state, dispatch }: EvaluationModelProps) {
|
||||||
|
const handleToggleFactor = (factorId: string) => {
|
||||||
|
dispatch({ type: 'TOGGLE_FACTOR', payload: factorId });
|
||||||
|
const factor = state.factors.find(f => f.id === factorId);
|
||||||
|
if (factor) {
|
||||||
|
toast.success(`${factor.name} 已${factor.enabled ? '禁用' : '启用'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFactorIcon = (category: string) => {
|
||||||
|
const icons = {
|
||||||
|
soil: <Target className="w-4 h-4" />,
|
||||||
|
climate: <Sun className="w-4 h-4" />,
|
||||||
|
topography: <Mountain className="w-4 h-4" />,
|
||||||
|
infrastructure: <Building className="w-4 h-4" />
|
||||||
|
};
|
||||||
|
return icons[category as keyof typeof icons] || <Settings className="w-4 h-4" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFactorCategoryColor = (category: string) => {
|
||||||
|
const colors = {
|
||||||
|
soil: 'bg-amber-50 border-amber-200 text-amber-700',
|
||||||
|
climate: 'bg-blue-50 border-blue-200 text-blue-700',
|
||||||
|
topography: 'bg-green-50 border-green-200 text-green-700',
|
||||||
|
infrastructure: 'bg-purple-50 border-purple-200 text-purple-700'
|
||||||
|
};
|
||||||
|
return colors[category as keyof typeof colors] || 'bg-gray-50 border-gray-200 text-gray-700';
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabledFactors = state.factors.filter(f => f.enabled);
|
||||||
|
const enabledFactorCount = enabledFactors.length;
|
||||||
|
const totalFactorCount = state.factors.length;
|
||||||
|
|
||||||
|
const factorCategories = [
|
||||||
|
{
|
||||||
|
key: 'soil',
|
||||||
|
name: '土壤因子',
|
||||||
|
icon: <Target className="w-5 h-5 text-amber-600" />,
|
||||||
|
factors: state.factors.filter(f => f.category === 'soil')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'climate',
|
||||||
|
name: '气候因子',
|
||||||
|
icon: <Sun className="w-5 h-5 text-blue-600" />,
|
||||||
|
factors: state.factors.filter(f => f.category === 'climate')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'topography',
|
||||||
|
name: '地形因子',
|
||||||
|
icon: <Mountain className="w-5 h-5 text-green-600" />,
|
||||||
|
factors: state.factors.filter(f => f.category === 'topography')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'infrastructure',
|
||||||
|
name: '基础设施因子',
|
||||||
|
icon: <Building className="w-5 h-5 text-purple-600" />,
|
||||||
|
factors: state.factors.filter(f => f.category === 'infrastructure')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
|
||||||
|
<Brain className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">评价模型配置</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">选择和配置评价因子</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="px-3 py-1">
|
||||||
|
{enabledFactorCount}/{totalFactorCount} 个因子
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
// 启用所有因子
|
||||||
|
state.factors.forEach(factor => {
|
||||||
|
if (!factor.enabled) {
|
||||||
|
dispatch({ type: 'TOGGLE_FACTOR', payload: factor.id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
toast.success('已启用所有评价因子');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
全部启用
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 评价因子配置区域 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{factorCategories.map((category) => (
|
||||||
|
<div key={category.key} className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{category.icon}
|
||||||
|
<h4 className="font-medium">{category.name}</h4>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{category.factors.filter(f => f.enabled).length}/{category.factors.length}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{category.factors.map((factor) => (
|
||||||
|
<div
|
||||||
|
key={factor.id}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-all ${
|
||||||
|
factor.enabled
|
||||||
|
? getFactorCategoryColor(factor.category)
|
||||||
|
: 'bg-gray-50 border-gray-200 text-gray-400 opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
{getFactorIcon(factor.category)}
|
||||||
|
<h5 className="font-medium text-sm">{factor.name}</h5>
|
||||||
|
{factor.enabled ? (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-4 h-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
{factor.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
权重: {Math.round(factor.weight * 100)}%
|
||||||
|
</Badge>
|
||||||
|
<Switch
|
||||||
|
checked={factor.enabled}
|
||||||
|
onCheckedChange={() => handleToggleFactor(factor.id)}
|
||||||
|
disabled={state.isBatchAnalyzing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模型配置说明 */}
|
||||||
|
<div className="mt-6 p-4 bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Info className="w-5 h-5 text-purple-600 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-purple-800 dark:text-purple-200">
|
||||||
|
评价模型说明
|
||||||
|
</h4>
|
||||||
|
<div className="text-sm text-purple-700 dark:text-purple-300 mt-2 space-y-2">
|
||||||
|
<p>
|
||||||
|
<strong>多因子加权评价模型</strong>采用层次分析法(AHP)和专家评分法,
|
||||||
|
结合农业生产实际需求,构建科学的评价指标体系。
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium mb-2">评价因子选择原则:</h5>
|
||||||
|
<ul className="text-xs space-y-1">
|
||||||
|
<li>• <strong>主导性</strong>:选择对作物生长起决定性作用的因子</li>
|
||||||
|
<li>• <strong>独立性</strong>:避免因子间的重复和高度相关性</li>
|
||||||
|
<li>• <strong>可获取性</strong>:确保数据能够准确获取和更新</li>
|
||||||
|
<li>• <strong>稳定性</strong>:优先选择相对稳定的评价因子</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium mb-2">权重确定方法:</h5>
|
||||||
|
<ul className="text-xs space-y-1">
|
||||||
|
<li>• <strong>专家打分</strong>:邀请农业专家进行权重评分</li>
|
||||||
|
<li>• <strong>层次分析</strong>:构建判断矩阵确定权重</li>
|
||||||
|
<li>• <strong>实地验证</strong>:结合实际产量数据校准权重</li>
|
||||||
|
<li>• <strong>动态调整</strong>:根据作物类型和生产目标调整</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 启用因子过少警告 */}
|
||||||
|
{enabledFactorCount < 6 && (
|
||||||
|
<div className="mt-4 p-4 bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Info className="w-5 h-5 text-yellow-600 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-yellow-800 dark:text-yellow-200">
|
||||||
|
评价因子配置建议
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
|
||||||
|
当前仅启用了 {enabledFactorCount} 个评价因子,建议至少启用 6-8 个因子以获得更准确的评价结果。
|
||||||
|
确保各类别都有代表性因子参与评价。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Settings,
|
||||||
|
Sliders,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Info
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface FactorConfigurationProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
isDialog?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FactorConfiguration({ state, dispatch, isDialog = false }: FactorConfigurationProps) {
|
||||||
|
const categories = [
|
||||||
|
{ id: 'soil', name: '土壤条件', color: 'blue' },
|
||||||
|
{ id: 'climate', name: '气候条件', color: 'green' },
|
||||||
|
{ id: 'topography', name: '地形条件', color: 'orange' },
|
||||||
|
{ id: 'infrastructure', name: '基础设施', color: 'purple' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleToggleFactor = (factorId: string) => {
|
||||||
|
dispatch({ type: 'TOGGLE_FACTOR', payload: factorId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateWeight = (factorId: string, weight: number) => {
|
||||||
|
dispatch({ type: 'UPDATE_FACTOR_WEIGHT', payload: { id: factorId, weight } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetToDefaults = () => {
|
||||||
|
// 重置为默认权重
|
||||||
|
const defaultWeights = {
|
||||||
|
'soil_ph': 0.15,
|
||||||
|
'soil_organic_matter': 0.12,
|
||||||
|
'soil_nutrients': 0.18,
|
||||||
|
'soil_texture': 0.10,
|
||||||
|
'temperature': 0.15,
|
||||||
|
'rainfall': 0.12,
|
||||||
|
'sunlight': 0.10,
|
||||||
|
'elevation': 0.08,
|
||||||
|
'slope': 0.06,
|
||||||
|
'irrigation': 0.12,
|
||||||
|
'accessibility': 0.05
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(defaultWeights).forEach(([id, weight]) => {
|
||||||
|
dispatch({ type: 'UPDATE_FACTOR_WEIGHT', payload: { id, weight } });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalWeight = () => {
|
||||||
|
return state.factors.reduce((sum, factor) => sum + factor.weight, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryTotal = (categoryId: string) => {
|
||||||
|
return state.factors
|
||||||
|
.filter(f => f.category === categoryId && f.enabled)
|
||||||
|
.reduce((sum, f) => sum + f.weight, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryColors = {
|
||||||
|
soil: 'blue',
|
||||||
|
climate: 'green',
|
||||||
|
topography: 'orange',
|
||||||
|
infrastructure: 'purple'
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 权重总览 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium">权重分配总览</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">总权重:</span>
|
||||||
|
<Badge variant={Math.abs(getTotalWeight() - 1) < 0.01 ? 'default' : 'destructive'}>
|
||||||
|
{Math.round(getTotalWeight() * 100)}%
|
||||||
|
</Badge>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleResetToDefaults}>
|
||||||
|
重置默认
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{categories.map(category => {
|
||||||
|
const totalWeight = getCategoryTotal(category.id);
|
||||||
|
return (
|
||||||
|
<div key={category.id}>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium">{category.name}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{Math.round(totalWeight * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={totalWeight * 100}
|
||||||
|
className={`h-2 ${
|
||||||
|
category.id === 'soil' ? 'bg-blue-100' :
|
||||||
|
category.id === 'climate' ? 'bg-green-100' :
|
||||||
|
category.id === 'topography' ? 'bg-orange-100' :
|
||||||
|
'bg-purple-100'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 因子详细配置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{categories.map(category => (
|
||||||
|
<Card key={category.id} className="p-4">
|
||||||
|
<h3 className="font-medium mb-4 flex items-center gap-2">
|
||||||
|
<div className={`w-3 h-3 rounded-full bg-${categoryColors[category.id]}-500`} />
|
||||||
|
{category.name}
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{state.factors.filter(f => f.category === category.id).length} 个因子
|
||||||
|
</Badge>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{state.factors
|
||||||
|
.filter(factor => factor.category === category.id)
|
||||||
|
.map(factor => (
|
||||||
|
<div key={factor.id} className="border rounded-lg p-4">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-medium">{factor.name}</h4>
|
||||||
|
{factor.enabled ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-4 h-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{factor.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-lg font-bold">
|
||||||
|
{Math.round(factor.weight * 100)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">权重</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
checked={factor.enabled}
|
||||||
|
onCheckedChange={() => handleToggleFactor(factor.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重调节 */}
|
||||||
|
{factor.enabled && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">权重调节</span>
|
||||||
|
<span>{Math.round(factor.weight * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[factor.weight * 100]}
|
||||||
|
onValueChange={([value]) =>
|
||||||
|
handleUpdateWeight(factor.id, value / 100)
|
||||||
|
}
|
||||||
|
max={50}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 配置说明 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Info className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="font-medium mb-2">配置说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• 权重总和应保持100%,系统会自动计算各因子的影响程度</li>
|
||||||
|
<li>• 启用/禁用因子来控制评价维度,禁用的因子不参与计算</li>
|
||||||
|
<li>• 土壤条件、气候条件、地形条件、基础设施为四大评价维度</li>
|
||||||
|
<li>• 可以根据具体需求调整各因子的权重,建议咨询农业专家</li>
|
||||||
|
<li>• 点击"重置默认"可以恢复到推荐的标准权重配置</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDialog) {
|
||||||
|
return (
|
||||||
|
<Dialog open={state.showFactorDialog} onOpenChange={() => dispatch({ type: 'TOGGLE_FACTOR_DIALOG' })}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Settings className="w-5 h-5" />
|
||||||
|
评价因子配置
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{content}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-medium">评价因子配置</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline">
|
||||||
|
启用 {state.factors.filter(f => f.enabled).length} / {state.factors.length}
|
||||||
|
</Badge>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleResetToDefaults}>
|
||||||
|
重置默认
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Settings,
|
||||||
|
SlidersHorizontal,
|
||||||
|
TrendingUp,
|
||||||
|
Target,
|
||||||
|
Droplet,
|
||||||
|
Sun,
|
||||||
|
Mountain,
|
||||||
|
Building,
|
||||||
|
RefreshCw
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface FactorWeightsProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FactorWeights({ state, dispatch }: FactorWeightsProps) {
|
||||||
|
const totalWeight = Math.round(
|
||||||
|
(state.weightConfig.soil + state.weightConfig.climate +
|
||||||
|
state.weightConfig.topography + state.weightConfig.infrastructure) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleWeightChange = (category: keyof SpatialAnalysisState['weightConfig'], value: number[]) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_WEIGHT_CONFIG',
|
||||||
|
payload: {
|
||||||
|
...state.weightConfig,
|
||||||
|
[category]: value[0]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetWeights = () => {
|
||||||
|
dispatch({ type: 'RESET_WEIGHTS' });
|
||||||
|
toast.success('权重配置已重置为默认值');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeightColor = (category: string) => {
|
||||||
|
const colors = {
|
||||||
|
soil: 'text-amber-600 bg-amber-50 border-amber-200',
|
||||||
|
climate: 'text-blue-600 bg-blue-50 border-blue-200',
|
||||||
|
topography: 'text-green-600 bg-green-50 border-green-200',
|
||||||
|
infrastructure: 'text-purple-600 bg-purple-50 border-purple-200'
|
||||||
|
};
|
||||||
|
return colors[category as keyof typeof colors] || 'text-gray-600 bg-gray-50 border-gray-200';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeightIcon = (category: string) => {
|
||||||
|
const icons = {
|
||||||
|
soil: <Target className="w-5 h-5" />,
|
||||||
|
climate: <Sun className="w-5 h-5" />,
|
||||||
|
topography: <Mountain className="w-5 h-5" />,
|
||||||
|
infrastructure: <Building className="w-5 h-5" />
|
||||||
|
};
|
||||||
|
return icons[category as keyof typeof icons] || <Settings className="w-5 h-5" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const factorCategories = [
|
||||||
|
{
|
||||||
|
key: 'soil' as const,
|
||||||
|
name: '土壤因子',
|
||||||
|
icon: <Target className="w-5 h-5 text-amber-600" />,
|
||||||
|
description: 'pH值、有机质、全氮、全磷、全钾、土壤质地等',
|
||||||
|
factors: state.factors.filter(f => f.category === 'soil')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'climate' as const,
|
||||||
|
name: '气候因子',
|
||||||
|
icon: <Sun className="w-5 h-5 text-blue-600" />,
|
||||||
|
description: '年均温度、年降雨量、日照时数、无霜期等',
|
||||||
|
factors: state.factors.filter(f => f.category === 'climate')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'topography' as const,
|
||||||
|
name: '地形因子',
|
||||||
|
icon: <Mountain className="w-5 h-5 text-green-600" />,
|
||||||
|
description: '海拔高度、坡度、坡向、地貌类型等',
|
||||||
|
factors: state.factors.filter(f => f.category === 'topography')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'infrastructure' as const,
|
||||||
|
name: '基础设施因子',
|
||||||
|
icon: <Building className="w-5 h-5 text-purple-600" />,
|
||||||
|
description: '灌溉条件、交通便利性、排水条件等',
|
||||||
|
factors: state.factors.filter(f => f.category === 'infrastructure')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||||
|
<SlidersHorizontal className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">因子权重配置</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">调整各类因子的权重占比</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant={totalWeight === 100 ? "default" : "destructive"}
|
||||||
|
className="px-3 py-1"
|
||||||
|
>
|
||||||
|
总权重: {totalWeight}%
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleResetWeights}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重配置区域 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
{factorCategories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category.key}
|
||||||
|
className={`p-4 rounded-lg border-2 ${getWeightColor(category.key)}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{getWeightIcon(category.key)}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">{category.name}</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{category.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{Math.round(state.weightConfig[category.key] * 100)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{category.factors.length} 个因子
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>权重调整</span>
|
||||||
|
<span>{Math.round(state.weightConfig[category.key] * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[state.weightConfig[category.key] * 100]}
|
||||||
|
onValueChange={(value) => handleWeightChange(category.key, value)}
|
||||||
|
max={60}
|
||||||
|
min={5}
|
||||||
|
step={5}
|
||||||
|
className="w-full"
|
||||||
|
disabled={state.isBatchAnalyzing}
|
||||||
|
/>
|
||||||
|
<Progress
|
||||||
|
value={state.weightConfig[category.key] * 100}
|
||||||
|
className="h-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 显示该类别下的主要因子 */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-current/20">
|
||||||
|
<div className="text-xs font-medium mb-2">主要评价因子:</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{category.factors.slice(0, 3).map((factor) => (
|
||||||
|
<Badge
|
||||||
|
key={factor.id}
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs px-2 py-0.5"
|
||||||
|
>
|
||||||
|
{factor.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{category.factors.length > 3 && (
|
||||||
|
<Badge variant="outline" className="text-xs px-2 py-0.5">
|
||||||
|
+{category.factors.length - 3}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重总和警告 */}
|
||||||
|
{totalWeight !== 100 && (
|
||||||
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<TrendingUp className="w-5 h-5 text-yellow-600 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-yellow-800 dark:text-yellow-200">
|
||||||
|
权重总和验证
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
|
||||||
|
所有因子权重总和必须为100%才能进行批量分析。
|
||||||
|
当前总和为 {totalWeight}%,{totalWeight > 100 ? '超出' : '还差'} {Math.abs(totalWeight - 100)}%。
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-yellow-600 dark:text-yellow-400 mt-2">
|
||||||
|
💡 建议权重分配:土壤因子(35%)、气候因子(30%)、地形因子(20%)、基础设施因子(15%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 权重说明 */}
|
||||||
|
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Settings className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-blue-800 dark:text-blue-200">
|
||||||
|
权重配置说明
|
||||||
|
</h4>
|
||||||
|
<ul className="text-sm text-blue-700 dark:text-blue-300 mt-2 space-y-1">
|
||||||
|
<li>• <strong>土壤因子</strong>:反映地块基础肥力和物理化学性质</li>
|
||||||
|
<li>• <strong>气候因子</strong>:决定光热水的匹配度和作物适应性</li>
|
||||||
|
<li>• <strong>地形因子</strong>:影响水土保持和机械化作业条件</li>
|
||||||
|
<li>• <strong>基础设施因子</strong>:体现生产条件和集约化水平</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-xs text-blue-600 dark:text-blue-400 mt-3">
|
||||||
|
权重配置将直接影响最终适宜性指数的计算结果,请根据实际生产需求科学分配。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { MapPin, CheckCircle, Circle, Loader2, XCircle } from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface LandBlockSelectorProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
isDialog?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LandBlockSelector({ state, dispatch, isDialog = false }: LandBlockSelectorProps) {
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (state.selectedBlocks.length === state.landBlocks.length) {
|
||||||
|
dispatch({ type: 'SET_SELECTED_BLOCKS', payload: [] });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_SELECTED_BLOCKS', payload: state.landBlocks.map(b => b.id) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBlockScore = (blockId: string) => {
|
||||||
|
const result = state.analysisResults.find(r => r.blockId === blockId);
|
||||||
|
return result?.overallScore || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScoreColor = (score: number) => {
|
||||||
|
if (score >= 80) return 'text-green-600';
|
||||||
|
if (score >= 60) return 'text-yellow-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScoreLabel = (score: number) => {
|
||||||
|
if (score >= 80) return '优秀';
|
||||||
|
if (score >= 60) return '良好';
|
||||||
|
if (score >= 40) return '一般';
|
||||||
|
return '较差';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDialog) {
|
||||||
|
// 对话框模式下的简化版本
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium">选择分析地块</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
>
|
||||||
|
{state.selectedBlocks.length === state.landBlocks.length ? '取消全选' : '全选'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{state.landBlocks.map(block => {
|
||||||
|
const isSelected = state.selectedBlocks.includes(block.id);
|
||||||
|
const score = getBlockScore(block.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={block.id}
|
||||||
|
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||||
|
isSelected ? 'bg-blue-50 border-blue-200' : 'hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
onClick={() => dispatch({ type: 'TOGGLE_BLOCK_SELECTION', payload: block.id })}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Checkbox checked={isSelected} />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{block.name}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{block.area} 亩 · {block.soilType} · {block.irrigation}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{score && (
|
||||||
|
<Badge className={getScoreColor(score)}>
|
||||||
|
{score}分
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-medium">地块选择</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
>
|
||||||
|
{state.selectedBlocks.length === state.landBlocks.length ? '取消全选' : '全选'}
|
||||||
|
</Button>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
已选择 {state.selectedBlocks.length} / {state.landBlocks.length}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地块列表 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{state.landBlocks.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<MapPin className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>暂无地块数据</p>
|
||||||
|
<p className="text-sm mt-1">请先添加地块信息</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
state.landBlocks.map(block => {
|
||||||
|
const isSelected = state.selectedBlocks.includes(block.id);
|
||||||
|
const score = getBlockScore(block.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={block.id}
|
||||||
|
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800'
|
||||||
|
: 'hover:bg-gray-50 dark:hover:bg-gray-900 border-gray-200 dark:border-gray-700'
|
||||||
|
}`}
|
||||||
|
onClick={() => dispatch({ type: 'TOGGLE_BLOCK_SELECTION', payload: block.id })}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start gap-3 flex-1">
|
||||||
|
<div className="pt-1">
|
||||||
|
{isSelected ? (
|
||||||
|
<CheckCircle className="w-5 h-5 text-blue-600" />
|
||||||
|
) : (
|
||||||
|
<Circle className="w-5 h-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h4 className="font-medium">{block.name}</h4>
|
||||||
|
{block.currentCrop && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{block.currentCrop}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地块基本信息 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-muted-foreground">面积:</span>
|
||||||
|
<span className="font-medium">{block.area} 亩</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-muted-foreground">土壤:</span>
|
||||||
|
<span className="font-medium">{block.soilType}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-muted-foreground">灌溉:</span>
|
||||||
|
<span className="font-medium">{block.irrigation}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-muted-foreground">海拔:</span>
|
||||||
|
<span className="font-medium">{block.elevation}m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 土壤指标 */}
|
||||||
|
<div className="mt-3 grid grid-cols-2 md:grid-cols-5 gap-3 text-xs">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">pH:</span>
|
||||||
|
<span className="ml-1 font-medium">{block.pH}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">有机质:</span>
|
||||||
|
<span className="ml-1 font-medium">{block.organicMatter}%</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">氮:</span>
|
||||||
|
<span className="ml-1 font-medium">{block.nitrogen}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">磷:</span>
|
||||||
|
<span className="ml-1 font-medium">{block.phosphorus}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">钾:</span>
|
||||||
|
<span className="ml-1 font-medium">{block.potassium}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分析状态和适宜性指数 */}
|
||||||
|
{(score || block.analysisStatus) && (
|
||||||
|
<div className="ml-4 text-center">
|
||||||
|
{block.analysisStatus === 'analyzing' && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Loader2 className="w-6 h-6 text-blue-500 animate-spin" />
|
||||||
|
<div className="text-xs text-blue-500 mt-1">分析中</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{block.analysisStatus === 'failed' && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<XCircle className="w-6 h-6 text-red-500" />
|
||||||
|
<div className="text-xs text-red-500 mt-1">分析失败</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{block.analysisStatus === 'completed' && block.suitabilityIndex && (
|
||||||
|
<div>
|
||||||
|
<div className={`text-2xl font-bold ${getScoreColor(block.suitabilityIndex)}`}>
|
||||||
|
{block.suitabilityIndex}
|
||||||
|
</div>
|
||||||
|
<div className={`text-xs ${getScoreColor(block.suitabilityIndex)}`}>
|
||||||
|
适宜性指数
|
||||||
|
</div>
|
||||||
|
<Progress value={block.suitabilityIndex} className="w-16 h-2 mt-1" />
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
{block.lastAnalyzed}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{score && !block.analysisStatus && (
|
||||||
|
<div>
|
||||||
|
<div className={`text-2xl font-bold ${getScoreColor(score)}`}>
|
||||||
|
{score}
|
||||||
|
</div>
|
||||||
|
<div className={`text-xs ${getScoreColor(score)}`}>
|
||||||
|
{getScoreLabel(score)}
|
||||||
|
</div>
|
||||||
|
<Progress value={score} className="w-16 h-2 mt-1" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 选中地块统计 */}
|
||||||
|
{state.selectedBlocks.length > 0 && (
|
||||||
|
<div className="mt-6 p-4 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<h4 className="font-medium text-green-900 dark:text-green-100 mb-2">选中地块统计</h4>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm text-green-800 dark:text-green-200">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground text-green-700 dark:text-green-300">地块数量:</span>
|
||||||
|
<span className="ml-1 font-medium">{state.selectedBlocks.length} 个</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground text-green-700 dark:text-green-300">总面积:</span>
|
||||||
|
<span className="ml-1 font-medium">
|
||||||
|
{state.landBlocks
|
||||||
|
.filter(b => state.selectedBlocks.includes(b.id))
|
||||||
|
.reduce((sum, b) => sum + b.area, 0)
|
||||||
|
.toFixed(1)} 亩
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground text-green-700 dark:text-green-300">平均得分:</span>
|
||||||
|
<span className="ml-1 font-medium">
|
||||||
|
{state.analysisResults.length > 0
|
||||||
|
? Math.round(
|
||||||
|
state.analysisResults
|
||||||
|
.filter(r => state.selectedBlocks.includes(r.blockId))
|
||||||
|
.reduce((sum, r) => sum + r.overallScore, 0) /
|
||||||
|
state.analysisResults.filter(r => state.selectedBlocks.includes(r.blockId)).length
|
||||||
|
)
|
||||||
|
: '--'
|
||||||
|
} 分
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground text-green-700 dark:text-green-300">土壤类型:</span>
|
||||||
|
<span className="ml-1 font-medium">
|
||||||
|
{[...new Set(
|
||||||
|
state.landBlocks
|
||||||
|
.filter(b => state.selectedBlocks.includes(b.id))
|
||||||
|
.map(b => b.soilType)
|
||||||
|
)].join(', ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
import BatchEvaluationPanel from './BatchEvaluationPanel';
|
||||||
|
import AnalysisResults from './AnalysisResults';
|
||||||
|
import FactorWeights from './FactorWeights';
|
||||||
|
import EvaluationModel from './EvaluationModel';
|
||||||
|
|
||||||
|
interface SpatialAnalysisContentProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpatialAnalysisContent({ state, dispatch }: SpatialAnalysisContentProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 标题区域 */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl text-green-800 dark:text-green-200">自动化空间分析</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
多因子评价模型,批量生成地块适宜性指数并更新到数据库
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重配置面板 */}
|
||||||
|
<FactorWeights state={state} dispatch={dispatch} />
|
||||||
|
|
||||||
|
{/* 评价模型配置 */}
|
||||||
|
<EvaluationModel state={state} dispatch={dispatch} />
|
||||||
|
|
||||||
|
{/* 批量评价面板 */}
|
||||||
|
<BatchEvaluationPanel state={state} dispatch={dispatch} />
|
||||||
|
|
||||||
|
{/* 分析结果记录 */}
|
||||||
|
{state.analysisResults.length > 0 && (
|
||||||
|
<AnalysisResults state={state} dispatch={dispatch} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { MapPin, Layers, Navigation } from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface SpatialDistributionProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpatialDistribution({ state, dispatch }: SpatialDistributionProps) {
|
||||||
|
const getScoreColor = (score: number) => {
|
||||||
|
if (score >= 80) return 'bg-green-500';
|
||||||
|
if (score >= 60) return 'bg-yellow-500';
|
||||||
|
if (score >= 40) return 'bg-orange-500';
|
||||||
|
return 'bg-red-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScoreTextColor = (score: number) => {
|
||||||
|
if (score >= 80) return 'text-green-600';
|
||||||
|
if (score >= 60) return 'text-yellow-600';
|
||||||
|
if (score >= 40) return 'text-orange-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedBlocksWithScores = state.selectedBlocks.map(blockId => {
|
||||||
|
const block = state.landBlocks.find(b => b.id === blockId);
|
||||||
|
const result = state.analysisResults.find(r => r.blockId === blockId);
|
||||||
|
return { block, result };
|
||||||
|
}).filter(item => item.block);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<MapPin className="w-5 h-5" />
|
||||||
|
空间分布可视化
|
||||||
|
</h3>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{selectedBlocksWithScores.length} 个地块
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 简化的地图展示 */}
|
||||||
|
<div className="relative bg-gray-50 dark:bg-gray-900 rounded-lg p-8 min-h-[400px] flex items-center justify-center">
|
||||||
|
{selectedBlocksWithScores.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<MapPin className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>请先选择地块以查看空间分布</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative w-full h-full min-h-[350px]">
|
||||||
|
{/* 模拟地图背景 */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-green-50 to-blue-50 dark:from-green-950 dark:to-blue-950 rounded-lg opacity-30"></div>
|
||||||
|
|
||||||
|
{/* 地块分布 */}
|
||||||
|
<div className="relative h-full flex items-center justify-center">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 w-full max-w-2xl">
|
||||||
|
{selectedBlocksWithScores.map((item, index) => {
|
||||||
|
const positions = [
|
||||||
|
{ top: '20%', left: '15%' },
|
||||||
|
{ top: '25%', left: '60%' },
|
||||||
|
{ top: '60%', left: '25%' },
|
||||||
|
{ top: '65%', left: '70%' },
|
||||||
|
{ top: '40%', left: '40%' },
|
||||||
|
{ top: '30%', left: '80%' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const position = positions[index % positions.length];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.block!.id}
|
||||||
|
className="absolute transform -translate-x-1/2 -translate-y-1/2"
|
||||||
|
style={position}
|
||||||
|
>
|
||||||
|
{/* 地块标记 */}
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className={`w-12 h-12 ${getScoreColor(item.result?.overallScore || 0)} rounded-full opacity-80 animate-pulse`}></div>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<MapPin className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地块信息卡片 */}
|
||||||
|
<div className="absolute top-14 left-1/2 transform -translate-x-1/2 bg-white dark:bg-gray-800 border rounded-lg shadow-lg p-3 w-48 z-10">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="font-medium text-sm truncate">{item.block!.name}</h4>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`text-xs ${getScoreTextColor(item.result?.overallScore || 0)}`}
|
||||||
|
>
|
||||||
|
{item.result?.overallScore || '--'}分
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>面积:</span>
|
||||||
|
<span>{item.block!.area} 亩</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>土壤:</span>
|
||||||
|
<span>{item.block!.soilType}</span>
|
||||||
|
</div>
|
||||||
|
{item.result && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>土壤得分:</span>
|
||||||
|
<span>{Math.round(item.result.soilScore)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>气候得分:</span>
|
||||||
|
<span>{Math.round(item.result.climateScore)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{item.result?.recommendedCrops[0] && (
|
||||||
|
<div className="pt-1 border-t">
|
||||||
|
<span className="text-blue-600">推荐: {item.result.recommendedCrops[0].crop.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图例 */}
|
||||||
|
<div className="absolute bottom-4 left-4 bg-white dark:bg-gray-800 border rounded-lg p-3 shadow-lg">
|
||||||
|
<h4 className="font-medium text-sm mb-2 flex items-center gap-2">
|
||||||
|
<Layers className="w-4 h-4" />
|
||||||
|
适宜性等级
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-1 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
<span>优秀 (≥80分)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||||
|
<span>良好 (60-79分)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
|
||||||
|
<span>一般 (40-59分)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||||
|
<span>较差 (<40分)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 指北针 */}
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<div className="w-12 h-12 bg-white dark:bg-gray-800 border rounded-full shadow-lg flex items-center justify-center">
|
||||||
|
<Navigation className="w-6 h-6 text-gray-600 dark:text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
{selectedBlocksWithScores.length > 0 && (
|
||||||
|
<div className="mt-6 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{selectedBlocksWithScores.filter(item => (item.result?.overallScore || 0) >= 80).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">优秀地块</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{Math.round(
|
||||||
|
selectedBlocksWithScores.reduce((sum, item) => sum + (item.block?.area || 0), 0)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">总面积(亩)</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">
|
||||||
|
{selectedBlocksWithScores.length > 0
|
||||||
|
? Math.round(
|
||||||
|
selectedBlocksWithScores.reduce((sum, item) => sum + (item.result?.overallScore || 0), 0) /
|
||||||
|
selectedBlocksWithScores.length
|
||||||
|
)
|
||||||
|
: 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">平均得分</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-orange-600">
|
||||||
|
{[...new Set(
|
||||||
|
selectedBlocksWithScores
|
||||||
|
.filter(item => item.result?.recommendedCrops[0])
|
||||||
|
.map(item => item.result!.recommendedCrops[0].crop.name)
|
||||||
|
)].length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">推荐作物</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Sliders,
|
||||||
|
RotateCcw,
|
||||||
|
Info,
|
||||||
|
Scale
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
interface WeightConfigurationProps {
|
||||||
|
state: SpatialAnalysisState;
|
||||||
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
isDialog?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeightConfiguration({ state, dispatch, isDialog = false }: WeightConfigurationProps) {
|
||||||
|
const categories = [
|
||||||
|
{ id: 'soil', name: '土壤条件', icon: '🌱', color: 'blue' },
|
||||||
|
{ id: 'climate', name: '气候条件', icon: '🌤️', color: 'green' },
|
||||||
|
{ id: 'topography', name: '地形条件', icon: '⛰️', color: 'orange' },
|
||||||
|
{ id: 'infrastructure', name: '基础设施', icon: '🏗️', color: 'purple' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleUpdateWeight = (category: string, value: number) => {
|
||||||
|
const newWeightConfig = {
|
||||||
|
...state.weightConfig,
|
||||||
|
[category]: value / 100
|
||||||
|
};
|
||||||
|
dispatch({ type: 'SET_WEIGHT_CONFIG', payload: newWeightConfig });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetToDefaults = () => {
|
||||||
|
const defaultConfig = {
|
||||||
|
soil: 0.35,
|
||||||
|
climate: 0.30,
|
||||||
|
topography: 0.20,
|
||||||
|
infrastructure: 0.15
|
||||||
|
};
|
||||||
|
dispatch({ type: 'SET_WEIGHT_CONFIG', payload: defaultConfig });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBalanceWeights = () => {
|
||||||
|
// 自动平衡权重,确保总和为100%
|
||||||
|
const total = Object.values(state.weightConfig).reduce((sum, val) => sum + val, 0);
|
||||||
|
if (total > 0) {
|
||||||
|
const balancedConfig = Object.fromEntries(
|
||||||
|
Object.entries(state.weightConfig).map(([key, value]) => [key, value / total])
|
||||||
|
) as typeof state.weightConfig;
|
||||||
|
dispatch({ type: 'SET_WEIGHT_CONFIG', payload: balancedConfig });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalWeight = () => {
|
||||||
|
return Object.values(state.weightConfig).reduce((sum, val) => sum + val, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryConfigs = categories.map(category => ({
|
||||||
|
...category,
|
||||||
|
weight: state.weightConfig[category.id as keyof typeof state.weightConfig],
|
||||||
|
percentage: Math.round(state.weightConfig[category.id as keyof typeof state.weightConfig] * 100)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 权重总览 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Scale className="w-5 h-5" />
|
||||||
|
权重分配总览
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className={`text-2xl font-bold ${
|
||||||
|
Math.abs(getTotalWeight() - 1) < 0.01 ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}>
|
||||||
|
{Math.round(getTotalWeight() * 100)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">总权重</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleResetToDefaults}>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
重置默认
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleBalanceWeights}>
|
||||||
|
<Sliders className="w-4 h-4 mr-2" />
|
||||||
|
自动平衡
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重可视化 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{categoryConfigs.map(category => (
|
||||||
|
<div key={category.id} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">{category.icon}</span>
|
||||||
|
<span className="font-medium">{category.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg font-bold">{category.percentage}%</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
category.id === 'soil' ? 'border-blue-200 text-blue-600' :
|
||||||
|
category.id === 'climate' ? 'border-green-200 text-green-600' :
|
||||||
|
category.id === 'topography' ? 'border-orange-200 text-orange-600' :
|
||||||
|
'border-purple-200 text-purple-600'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{category.weight.toFixed(2)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Progress
|
||||||
|
value={category.weight * 100}
|
||||||
|
className={`h-3 ${
|
||||||
|
category.id === 'soil' ? 'bg-blue-100' :
|
||||||
|
category.id === 'climate' ? 'bg-green-100' :
|
||||||
|
category.id === 'topography' ? 'bg-orange-100' :
|
||||||
|
'bg-purple-100'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{Math.abs(getTotalWeight() - 1) > 0.01 && (
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">偏差</span>
|
||||||
|
<span className="text-red-600 font-medium">
|
||||||
|
{Math.abs((getTotalWeight() - 1) * 100).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={getTotalWeight() * 100} className="h-2 mt-1" />
|
||||||
|
<p className="text-xs text-red-600 mt-1">
|
||||||
|
权重总和不为100%,建议点击"自动平衡"调整
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 权重调节面板 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-6">权重调节</h3>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{categoryConfigs.map(category => (
|
||||||
|
<div key={category.id} className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-xl">{category.icon}</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">{category.name}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{category.id === 'soil' && '包括土壤pH、有机质、养分含量等指标'}
|
||||||
|
{category.id === 'climate' && '包括温度、降水、光照等气象条件'}
|
||||||
|
{category.id === 'topography' && '包括海拔、坡度等地形特征'}
|
||||||
|
{category.id === 'infrastructure' && '包括灌溉、交通等设施条件'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right min-w-[80px]">
|
||||||
|
<div className="text-2xl font-bold">{category.percentage}%</div>
|
||||||
|
<div className="text-xs text-muted-foreground">当前权重</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Slider
|
||||||
|
value={[category.weight * 100]}
|
||||||
|
onValueChange={([value]) => handleUpdateWeight(category.id, value)}
|
||||||
|
max={60}
|
||||||
|
min={5}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
|
<span>5%</span>
|
||||||
|
<span>60%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 权重配置说明 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Info className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="font-medium mb-2">权重配置说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>土壤条件 (35%)</strong>: 土壤是农业生产的基础,影响作物生长和养分供应</li>
|
||||||
|
<li>• <strong>气候条件 (30%)</strong>: 气候决定了作物的生长环境和适宜种植范围</li>
|
||||||
|
<li>• <strong>地形条件 (20%)</strong>: 地形影响水土保持、机械化作业和微气候</li>
|
||||||
|
<li>• <strong>基础设施 (15%)</strong>: 灌溉、交通等设施影响生产效率和成本</li>
|
||||||
|
<li>• 权重总和应保持100%,可根据实际情况调整各维度的重要性</li>
|
||||||
|
<li>• 建议在农业专家指导下进行权重配置,确保评价结果科学合理</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 预设配置 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-medium mb-4">快速预设配置</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: 'SET_WEIGHT_CONFIG',
|
||||||
|
payload: { soil: 0.4, climate: 0.3, topography: 0.2, infrastructure: 0.1 }
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-medium text-sm">粮食作物优先</div>
|
||||||
|
<div className="text-xs text-muted-foreground">土壤40% · 气候30%</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: 'SET_WEIGHT_CONFIG',
|
||||||
|
payload: { soil: 0.3, climate: 0.4, topography: 0.2, infrastructure: 0.1 }
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-medium text-sm">经济作物优先</div>
|
||||||
|
<div className="text-xs text-muted-foreground">气候40% · 土壤30%</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: 'SET_WEIGHT_CONFIG',
|
||||||
|
payload: { soil: 0.3, climate: 0.3, topography: 0.3, infrastructure: 0.1 }
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-medium text-sm">均衡发展</div>
|
||||||
|
<div className="text-xs text-muted-foreground">三要素各30%</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDialog) {
|
||||||
|
return (
|
||||||
|
<Dialog open={state.showWeightDialog} onOpenChange={() => dispatch({ type: 'TOGGLE_WEIGHT_DIALOG' })}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Sliders className="w-5 h-5" />
|
||||||
|
权重配置
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{content}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-medium">权重配置</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={Math.abs(getTotalWeight() - 1) < 0.01 ? 'default' : 'destructive'}>
|
||||||
|
总权重: {Math.round(getTotalWeight() * 100)}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SpatialAnalysisState, SpatialAnalysisAction, LandBlock, AnalysisResult, BatchAnalysisTask, EvaluationFactor } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
// 空间分析服务接口
|
||||||
|
interface SpatialAnalysisService {
|
||||||
|
analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: any): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
suitabilityIndex: number;
|
||||||
|
result?: AnalysisResult;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟空间分析服务
|
||||||
|
class MockSpatialAnalysisService implements SpatialAnalysisService {
|
||||||
|
async analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: any): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
suitabilityIndex: number;
|
||||||
|
result?: AnalysisResult;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
// 模拟API调用延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 1200));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟分析失败的情况 (10% 概率)
|
||||||
|
if (Math.random() < 0.1) {
|
||||||
|
throw new Error('空间分析服务暂时不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取各项因子数据
|
||||||
|
const factorData = await this.readFactorData(block);
|
||||||
|
|
||||||
|
// 进行加权计算
|
||||||
|
const suitabilityIndex = this.calculateWeightedScore(factorData, factors, weights);
|
||||||
|
|
||||||
|
// 生成详细分析结果
|
||||||
|
const result = this.generateAnalysisResult(block, factorData, suitabilityIndex, weights);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
suitabilityIndex,
|
||||||
|
result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
suitabilityIndex: 0,
|
||||||
|
error: error instanceof Error ? error.message : '分析失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readFactorData(block: LandBlock): Promise<any> {
|
||||||
|
// 模拟从各种数据源读取因子数据
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 土壤因子数据
|
||||||
|
soilFactors: {
|
||||||
|
ph: block.pH || (6.0 + Math.random() * 2),
|
||||||
|
organicMatter: block.organicMatter || (1.5 + Math.random() * 2),
|
||||||
|
nitrogen: block.nitrogen || (60 + Math.random() * 80),
|
||||||
|
phosphorus: block.phosphorus || (30 + Math.random() * 40),
|
||||||
|
potassium: block.potassium || (80 + Math.random() * 80),
|
||||||
|
soilType: block.soilType,
|
||||||
|
texture: this.getSoilTexture(block.soilType)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 气候因子数据 (模拟从气象API获取)
|
||||||
|
climateFactors: {
|
||||||
|
temperature: 15 + Math.random() * 20, // 年均温度
|
||||||
|
rainfall: 400 + Math.random() * 800, // 年降雨量
|
||||||
|
sunlight: 1600 + Math.random() * 800, // 年日照时数
|
||||||
|
humidity: 50 + Math.random() * 30,
|
||||||
|
frostFreeDays: 180 + Math.random() * 60
|
||||||
|
},
|
||||||
|
|
||||||
|
// 地形因子数据 (模拟从地形API获取)
|
||||||
|
topographyFactors: {
|
||||||
|
elevation: block.elevation || (20 + Math.random() * 100),
|
||||||
|
slope: block.slope || (Math.random() * 10),
|
||||||
|
aspect: Math.floor(Math.random() * 360), // 坡向
|
||||||
|
relief: Math.random() * 50, // 地形起伏度
|
||||||
|
drainageIndex: 0.3 + Math.random() * 0.7 // 排水指数
|
||||||
|
},
|
||||||
|
|
||||||
|
// 基础设施数据
|
||||||
|
infrastructureFactors: {
|
||||||
|
irrigationType: block.irrigation,
|
||||||
|
irrigationScore: this.getIrrigationScore(block.irrigation),
|
||||||
|
accessibility: 0.4 + Math.random() * 0.6, // 交通便利性
|
||||||
|
distanceToRoad: Math.random() * 5, // 到道路距离(km)
|
||||||
|
distanceToMarket: Math.random() * 20, // 到市场距离(km)
|
||||||
|
powerSupply: Math.random() > 0.3, // 电力供应
|
||||||
|
waterSupply: Math.random() > 0.2 // 水源供应
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSoilTexture(soilType: string): string {
|
||||||
|
const textureMap: { [key: string]: string } = {
|
||||||
|
'壤土': 'loam',
|
||||||
|
'砂壤土': 'sandy_loam',
|
||||||
|
'黏土': 'clay',
|
||||||
|
'砂土': 'sandy',
|
||||||
|
'砾质土': 'gravelly'
|
||||||
|
};
|
||||||
|
return textureMap[soilType] || 'loam';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIrrigationScore(irrigationType: string): number {
|
||||||
|
const scoreMap: { [key: string]: number } = {
|
||||||
|
'滴灌': 0.95,
|
||||||
|
'喷灌': 0.85,
|
||||||
|
'漫灌': 0.70,
|
||||||
|
'无灌溉': 0.30
|
||||||
|
};
|
||||||
|
return scoreMap[irrigationType] || 0.70;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateWeightedScore(factorData: any, factors: EvaluationFactor[], weights: any): number {
|
||||||
|
let totalScore = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
|
||||||
|
// 土壤因子得分计算
|
||||||
|
const soilWeight = weights.soil || 0.35;
|
||||||
|
const soilScore = this.calculateSoilScore(factorData.soilFactors, factors);
|
||||||
|
totalScore += soilScore * soilWeight;
|
||||||
|
totalWeight += soilWeight;
|
||||||
|
|
||||||
|
// 气候因子得分计算
|
||||||
|
const climateWeight = weights.climate || 0.30;
|
||||||
|
const climateScore = this.calculateClimateScore(factorData.climateFactors, factors);
|
||||||
|
totalScore += climateScore * climateWeight;
|
||||||
|
totalWeight += climateWeight;
|
||||||
|
|
||||||
|
// 地形因子得分计算
|
||||||
|
const topographyWeight = weights.topography || 0.20;
|
||||||
|
const topographyScore = this.calculateTopographyScore(factorData.topographyFactors, factors);
|
||||||
|
totalScore += topographyScore * topographyWeight;
|
||||||
|
totalWeight += topographyWeight;
|
||||||
|
|
||||||
|
// 基础设施因子得分计算
|
||||||
|
const infrastructureWeight = weights.infrastructure || 0.15;
|
||||||
|
const infrastructureScore = this.calculateInfrastructureScore(factorData.infrastructureFactors, factors);
|
||||||
|
totalScore += infrastructureScore * infrastructureWeight;
|
||||||
|
totalWeight += infrastructureWeight;
|
||||||
|
|
||||||
|
return Math.round(totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateSoilScore(soilFactors: any, factors: EvaluationFactor[]): number {
|
||||||
|
let score = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
// pH值评分
|
||||||
|
if (soilFactors.ph >= 6.0 && soilFactors.ph <= 7.5) {
|
||||||
|
score += 90;
|
||||||
|
} else if (soilFactors.ph >= 5.5 && soilFactors.ph <= 8.0) {
|
||||||
|
score += 70;
|
||||||
|
} else {
|
||||||
|
score += 40;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 有机质评分
|
||||||
|
if (soilFactors.organicMatter >= 2.5) {
|
||||||
|
score += 90;
|
||||||
|
} else if (soilFactors.organicMatter >= 1.5) {
|
||||||
|
score += 70;
|
||||||
|
} else {
|
||||||
|
score += 45;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 养分评分 (NPK)
|
||||||
|
const npkScore = Math.min(100, (soilFactors.nitrogen + soilFactors.phosphorus + soilFactors.potassium) / 3);
|
||||||
|
score += npkScore;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
return count > 0 ? score / count : 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateClimateScore(climateFactors: any, factors: EvaluationFactor[]): number {
|
||||||
|
let score = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
// 温度适宜性
|
||||||
|
if (climateFactors.temperature >= 15 && climateFactors.temperature <= 25) {
|
||||||
|
score += 90;
|
||||||
|
} else if (climateFactors.temperature >= 10 && climateFactors.temperature <= 30) {
|
||||||
|
score += 70;
|
||||||
|
} else {
|
||||||
|
score += 50;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 降雨适宜性
|
||||||
|
if (climateFactors.rainfall >= 500 && climateFactors.rainfall <= 1000) {
|
||||||
|
score += 90;
|
||||||
|
} else if (climateFactors.rainfall >= 300 && climateFactors.rainfall <= 1500) {
|
||||||
|
score += 70;
|
||||||
|
} else {
|
||||||
|
score += 45;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 日照充足性
|
||||||
|
if (climateFactors.sunlight >= 2000) {
|
||||||
|
score += 90;
|
||||||
|
} else if (climateFactors.sunlight >= 1600) {
|
||||||
|
score += 75;
|
||||||
|
} else {
|
||||||
|
score += 60;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
|
||||||
|
return count > 0 ? score / count : 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateTopographyScore(topographyFactors: any, factors: EvaluationFactor[]): number {
|
||||||
|
let score = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
// 坡度评分
|
||||||
|
if (topographyFactors.slope <= 3) {
|
||||||
|
score += 95;
|
||||||
|
} else if (topographyFactors.slope <= 6) {
|
||||||
|
score += 80;
|
||||||
|
} else if (topographyFactors.slope <= 15) {
|
||||||
|
score += 60;
|
||||||
|
} else {
|
||||||
|
score += 30;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 海拔适宜性
|
||||||
|
if (topographyFactors.elevation <= 100) {
|
||||||
|
score += 90;
|
||||||
|
} else if (topographyFactors.elevation <= 300) {
|
||||||
|
score += 80;
|
||||||
|
} else if (topographyFactors.elevation <= 600) {
|
||||||
|
score += 65;
|
||||||
|
} else {
|
||||||
|
score += 45;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 排水条件
|
||||||
|
score += topographyFactors.drainageIndex * 100;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
return count > 0 ? score / count : 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateInfrastructureScore(infrastructureFactors: any, factors: EvaluationFactor[]): number {
|
||||||
|
let score = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
// 灌溉条件
|
||||||
|
score += infrastructureFactors.irrigationScore * 100;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 交通便利性
|
||||||
|
score += infrastructureFactors.accessibility * 100;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// 基础设施完善度
|
||||||
|
const infrastructureScore =
|
||||||
|
(infrastructureFactors.powerSupply ? 50 : 0) +
|
||||||
|
(infrastructureFactors.waterSupply ? 50 : 0);
|
||||||
|
score += infrastructureScore;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
return count > 0 ? score / count : 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateAnalysisResult(block: LandBlock, factorData: any, suitabilityIndex: number, weights: any): AnalysisResult {
|
||||||
|
const soilScore = this.calculateSoilScore(factorData.soilFactors, []);
|
||||||
|
const climateScore = this.calculateClimateScore(factorData.climateFactors, []);
|
||||||
|
const topographyScore = this.calculateTopographyScore(factorData.topographyFactors, []);
|
||||||
|
const infrastructureScore = this.calculateInfrastructureScore(factorData.infrastructureFactors, []);
|
||||||
|
|
||||||
|
// 生成作物推荐
|
||||||
|
const recommendedCrops = this.generateCropRecommendations(block, factorData, suitabilityIndex);
|
||||||
|
|
||||||
|
// 生成风险因素
|
||||||
|
const riskFactors = this.generateRiskFactors(block, factorData, suitabilityIndex);
|
||||||
|
|
||||||
|
// 生成改进建议
|
||||||
|
const improvementSuggestions = this.generateImprovementSuggestions(block, factorData, soilScore, climateScore, topographyScore, infrastructureScore);
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockId: block.id,
|
||||||
|
blockName: block.name,
|
||||||
|
overallScore: suitabilityIndex,
|
||||||
|
soilScore,
|
||||||
|
climateScore,
|
||||||
|
topographyScore,
|
||||||
|
infrastructureScore,
|
||||||
|
recommendedCrops,
|
||||||
|
riskFactors,
|
||||||
|
improvementSuggestions,
|
||||||
|
estimatedYield: Math.round(block.area * (suitabilityIndex / 100) * (6000 + Math.random() * 4000)),
|
||||||
|
economicReturn: Math.round(block.area * (suitabilityIndex / 100) * (8000 + Math.random() * 6000))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCropRecommendations(block: LandBlock, factorData: any, suitabilityIndex: number): any[] {
|
||||||
|
// 简化的作物推荐逻辑
|
||||||
|
const crops = [
|
||||||
|
{ name: '小麦', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 10 - 5) },
|
||||||
|
{ name: '玉米', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 15 - 5) },
|
||||||
|
{ name: '水稻', suitabilityScore: factorData.climateFactors.rainfall > 800 ? Math.min(95, suitabilityIndex + Math.random() * 10) : Math.max(40, suitabilityIndex - 20) },
|
||||||
|
{ name: '大豆', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 8 - 4) },
|
||||||
|
{ name: '棉花', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 12 - 6) }
|
||||||
|
];
|
||||||
|
|
||||||
|
return crops
|
||||||
|
.map(crop => ({
|
||||||
|
crop: {
|
||||||
|
id: crop.name.toLowerCase(),
|
||||||
|
name: crop.name,
|
||||||
|
category: '粮食作物',
|
||||||
|
growthCycle: 100 + Math.floor(Math.random() * 50),
|
||||||
|
economicValue: 2000 + Math.floor(Math.random() * 6000),
|
||||||
|
riskLevel: crop.suitabilityScore > 70 ? 'low' : crop.suitabilityScore > 50 ? 'medium' : 'high' as const
|
||||||
|
},
|
||||||
|
suitabilityScore: Math.round(crop.suitabilityScore),
|
||||||
|
matchPercentage: Math.round(crop.suitabilityScore),
|
||||||
|
advantages: crop.suitabilityScore > 70 ? ['土壤条件适宜', '气候匹配度高'] : ['需要改善条件'],
|
||||||
|
disadvantages: crop.suitabilityScore < 60 ? ['土壤需要改良', '气候条件一般'] : [],
|
||||||
|
managementNotes: ['注意合理施肥', '加强田间管理']
|
||||||
|
}))
|
||||||
|
.filter(crop => crop.suitabilityScore > 40)
|
||||||
|
.sort((a, b) => b.suitabilityScore - a.suitabilityScore)
|
||||||
|
.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRiskFactors(block: LandBlock, factorData: any, suitabilityIndex: number): string[] {
|
||||||
|
const risks = [];
|
||||||
|
|
||||||
|
if (suitabilityIndex < 40) {
|
||||||
|
risks.push('综合条件较差,种植风险较高');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factorData.soilFactors.ph < 6.0 || factorData.soilFactors.ph > 7.5) {
|
||||||
|
risks.push('土壤pH值偏离理想范围');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factorData.soilFactors.organicMatter < 1.5) {
|
||||||
|
risks.push('土壤有机质含量偏低');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factorData.topographyFactors.slope > 8) {
|
||||||
|
risks.push('坡度较大,存在水土流失风险');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factorData.infrastructureFactors.irrigationScore < 0.5) {
|
||||||
|
risks.push('灌溉条件不足,依赖自然降水');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factorData.climateFactors.rainfall < 400) {
|
||||||
|
risks.push('降雨量偏少,干旱风险较高');
|
||||||
|
}
|
||||||
|
|
||||||
|
return risks.length > 0 ? risks : ['无明显风险因素'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateImprovementSuggestions(block: LandBlock, factorData: any, soilScore: number, climateScore: number, topographyScore: number, infrastructureScore: number): string[] {
|
||||||
|
const suggestions = [];
|
||||||
|
|
||||||
|
if (soilScore < 70) {
|
||||||
|
suggestions.push('增施有机肥,改善土壤结构');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soilScore < 60) {
|
||||||
|
suggestions.push('进行土壤检测,针对性补充养分');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (climateScore < 70) {
|
||||||
|
suggestions.push('选择适应性更强的作物品种');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topographyScore < 70) {
|
||||||
|
suggestions.push('修建梯田或等高线种植');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infrastructureScore < 70) {
|
||||||
|
suggestions.push('完善灌溉和排水系统');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factorData.infrastructureFactors.accessibility < 0.6) {
|
||||||
|
suggestions.push('改善田间道路条件');
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions.length > 0 ? suggestions : ['当前条件良好,继续保持'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量分析管理器
|
||||||
|
export class BatchAnalysisManager {
|
||||||
|
private service: SpatialAnalysisService;
|
||||||
|
private dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
|
private isRunning: boolean = false;
|
||||||
|
private currentTask: BatchAnalysisTask | null = null;
|
||||||
|
|
||||||
|
constructor(dispatch: React.Dispatch<SpatialAnalysisAction>) {
|
||||||
|
this.service = new MockSpatialAnalysisService();
|
||||||
|
this.dispatch = dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startBatchAnalysis(
|
||||||
|
taskName: string,
|
||||||
|
factors: EvaluationFactor[],
|
||||||
|
weightConfig: any
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
throw new Error('已有分析任务正在运行');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 固定分析68个地块
|
||||||
|
const totalFields = 68;
|
||||||
|
const blockIds = Array.from({ length: totalFields }, (_, i) => `field-${i + 1}`);
|
||||||
|
|
||||||
|
// 创建批量分析任务
|
||||||
|
this.dispatch({
|
||||||
|
type: 'START_BATCH_ANALYSIS',
|
||||||
|
payload: { name: taskName, blockIds }
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取创建的任务
|
||||||
|
const task = await this.waitForTaskCreation();
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
this.currentTask = task;
|
||||||
|
|
||||||
|
// 更新任务状态为运行中
|
||||||
|
this.dispatch({
|
||||||
|
type: 'UPDATE_BATCH_TASK',
|
||||||
|
payload: {
|
||||||
|
taskId: task.id,
|
||||||
|
updates: {
|
||||||
|
status: 'running',
|
||||||
|
startedAt: new Date().toLocaleString('zh-CN'),
|
||||||
|
totalBlocks: totalFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 循环处理所有地块
|
||||||
|
const results: AnalysisResult[] = [];
|
||||||
|
let highSuitability = 0;
|
||||||
|
let mediumSuitability = 0;
|
||||||
|
let lowSuitability = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFields; i++) {
|
||||||
|
if (!this.isRunning) break; // 检查是否被取消
|
||||||
|
|
||||||
|
const fieldId = `field-${i + 1}`;
|
||||||
|
const fieldName = `地块${String.fromCharCode(65 + (i % 26))}${Math.floor(i / 26) + 1}`;
|
||||||
|
|
||||||
|
// 创建临时地块对象
|
||||||
|
const tempBlock: LandBlock = {
|
||||||
|
id: fieldId,
|
||||||
|
name: fieldName,
|
||||||
|
area: 10 + Math.random() * 20, // 随机面积10-30亩
|
||||||
|
location: { lat: 39.9042 + (Math.random() - 0.5) * 0.1, lng: 116.4074 + (Math.random() - 0.5) * 0.1 },
|
||||||
|
soilType: ['壤土', '砂壤土', '黏土'][Math.floor(Math.random() * 3)],
|
||||||
|
elevation: 20 + Math.random() * 100,
|
||||||
|
slope: Math.random() * 10,
|
||||||
|
irrigation: ['滴灌', '喷灌', '漫灌'][Math.floor(Math.random() * 3)],
|
||||||
|
pH: 6.0 + Math.random() * 2,
|
||||||
|
organicMatter: 1.5 + Math.random() * 2,
|
||||||
|
nitrogen: 60 + Math.random() * 80,
|
||||||
|
phosphorus: 30 + Math.random() * 40,
|
||||||
|
potassium: 80 + Math.random() * 80
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新当前处理的地块
|
||||||
|
this.dispatch({
|
||||||
|
type: 'UPDATE_BATCH_TASK',
|
||||||
|
payload: {
|
||||||
|
taskId: task.id,
|
||||||
|
updates: {
|
||||||
|
currentProcessingBlock: fieldName,
|
||||||
|
processedBlocks: i,
|
||||||
|
progress: Math.round((i / totalFields) * 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用空间分析服务
|
||||||
|
const analysisResult = await this.service.analyzeBlock(tempBlock, factors, weightConfig);
|
||||||
|
|
||||||
|
if (analysisResult.success && analysisResult.result) {
|
||||||
|
results.push(analysisResult.result);
|
||||||
|
|
||||||
|
// 统计适宜性等级
|
||||||
|
const score = analysisResult.suitabilityIndex;
|
||||||
|
if (score >= 80) {
|
||||||
|
highSuitability++;
|
||||||
|
} else if (score >= 60) {
|
||||||
|
mediumSuitability++;
|
||||||
|
} else {
|
||||||
|
lowSuitability++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务统计
|
||||||
|
this.dispatch({
|
||||||
|
type: 'UPDATE_BATCH_TASK',
|
||||||
|
payload: {
|
||||||
|
taskId: task.id,
|
||||||
|
updates: {
|
||||||
|
successCount: results.length,
|
||||||
|
failedCount: i + 1 - results.length,
|
||||||
|
progress: Math.round(((i + 1) / totalFields) * 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`分析地块 ${fieldName} 失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成批量分析
|
||||||
|
this.dispatch({
|
||||||
|
type: 'COMPLETE_BATCH_ANALYSIS',
|
||||||
|
payload: { taskId: task.id, results }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量分析失败:', error);
|
||||||
|
if (this.currentTask) {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'UPDATE_BATCH_TASK',
|
||||||
|
payload: {
|
||||||
|
taskId: this.currentTask.id,
|
||||||
|
updates: {
|
||||||
|
status: 'failed',
|
||||||
|
errorMessage: error instanceof Error ? error.message : '未知错误'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isRunning = false;
|
||||||
|
this.currentTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseAnalysis(): void {
|
||||||
|
if (this.currentTask) {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'PAUSE_BATCH_ANALYSIS',
|
||||||
|
payload: { taskId: this.currentTask.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeAnalysis(): void {
|
||||||
|
if (this.currentTask) {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'RESUME_BATCH_ANALYSIS',
|
||||||
|
payload: { taskId: this.currentTask.id }
|
||||||
|
});
|
||||||
|
this.isRunning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAnalysis(): void {
|
||||||
|
if (this.currentTask) {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'CANCEL_BATCH_ANALYSIS',
|
||||||
|
payload: { taskId: this.currentTask.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.isRunning = false;
|
||||||
|
this.currentTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForTaskCreation(): Promise<BatchAnalysisTask | null> {
|
||||||
|
// 等待任务创建完成 - 直接返回一个虚拟任务
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: '批量分析任务',
|
||||||
|
status: 'running',
|
||||||
|
createdAt: new Date().toLocaleString('zh-CN'),
|
||||||
|
totalBlocks: 68,
|
||||||
|
processedBlocks: 0,
|
||||||
|
successCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
selectedBlockIds: [],
|
||||||
|
weightConfig: {},
|
||||||
|
factorIds: [],
|
||||||
|
progress: 0
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BatchAnalysisManager;
|
||||||
@@ -0,0 +1,621 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// 空间分析数据类型定义
|
||||||
|
export interface LandBlock {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
area: number;
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
};
|
||||||
|
soilType: string;
|
||||||
|
currentCrop?: string;
|
||||||
|
elevation?: number;
|
||||||
|
slope?: number;
|
||||||
|
irrigation?: string;
|
||||||
|
pH?: number;
|
||||||
|
organicMatter?: number;
|
||||||
|
nitrogen?: number;
|
||||||
|
phosphorus?: number;
|
||||||
|
potassium?: number;
|
||||||
|
suitabilityIndex?: number; // 适宜性指数
|
||||||
|
lastAnalyzed?: string; // 最后分析时间
|
||||||
|
analysisStatus?: 'pending' | 'analyzing' | 'completed' | 'failed'; // 分析状态
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CropInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
growthCycle: number;
|
||||||
|
soilRequirements: {
|
||||||
|
pH: { min: number; max: number };
|
||||||
|
organicMatter: { min: number };
|
||||||
|
nitrogen: { min: number };
|
||||||
|
phosphorus: { min: number };
|
||||||
|
potassium: { min: number };
|
||||||
|
};
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { min: number; max: number };
|
||||||
|
rainfall: { min: number; max: number };
|
||||||
|
sunlight: { min: number };
|
||||||
|
};
|
||||||
|
economicValue: number;
|
||||||
|
riskLevel: 'low' | 'medium' | 'high';
|
||||||
|
suitableRegions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvaluationFactor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
weight: number;
|
||||||
|
enabled: boolean;
|
||||||
|
category: 'soil' | 'climate' | 'topography' | 'infrastructure';
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalysisResult {
|
||||||
|
blockId: string;
|
||||||
|
blockName: string;
|
||||||
|
overallScore: number;
|
||||||
|
soilScore: number;
|
||||||
|
climateScore: number;
|
||||||
|
topographyScore: number;
|
||||||
|
infrastructureScore: number;
|
||||||
|
recommendedCrops: CropRecommendation[];
|
||||||
|
riskFactors: string[];
|
||||||
|
improvementSuggestions: string[];
|
||||||
|
estimatedYield?: number;
|
||||||
|
economicReturn?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CropRecommendation {
|
||||||
|
crop: CropInfo;
|
||||||
|
suitabilityScore: number;
|
||||||
|
matchPercentage: number;
|
||||||
|
advantages: string[];
|
||||||
|
disadvantages: string[];
|
||||||
|
managementNotes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeightConfig {
|
||||||
|
soil: number;
|
||||||
|
climate: number;
|
||||||
|
topography: number;
|
||||||
|
infrastructure: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalysisReport {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
createdAt: string;
|
||||||
|
landBlocks: LandBlock[];
|
||||||
|
results: AnalysisResult[];
|
||||||
|
factors: EvaluationFactor[];
|
||||||
|
weightConfig: WeightConfig;
|
||||||
|
summary: {
|
||||||
|
totalBlocks: number;
|
||||||
|
averageScore: number;
|
||||||
|
topRecommendedCrops: Array<{ crop: string; frequency: number; avgScore: number }>;
|
||||||
|
riskDistribution: { low: number; medium: number; high: number };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量分析任务状态
|
||||||
|
export interface BatchAnalysisTask {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'paused';
|
||||||
|
createdAt: string;
|
||||||
|
startedAt?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
totalBlocks: number;
|
||||||
|
processedBlocks: number;
|
||||||
|
successCount: number;
|
||||||
|
failedCount: number;
|
||||||
|
selectedBlockIds: string[];
|
||||||
|
weightConfig: WeightConfig;
|
||||||
|
factorIds: string[];
|
||||||
|
progress: number;
|
||||||
|
currentProcessingBlock?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
export interface SpatialAnalysisState {
|
||||||
|
// 地块数据
|
||||||
|
landBlocks: LandBlock[];
|
||||||
|
selectedBlocks: string[];
|
||||||
|
|
||||||
|
// 评价因子
|
||||||
|
factors: EvaluationFactor[];
|
||||||
|
weightConfig: WeightConfig;
|
||||||
|
|
||||||
|
// 作物数据库
|
||||||
|
cropDatabase: CropInfo[];
|
||||||
|
selectedCrops: string[];
|
||||||
|
|
||||||
|
// 分析结果
|
||||||
|
analysisResults: AnalysisResult[];
|
||||||
|
currentReport: AnalysisReport | null;
|
||||||
|
|
||||||
|
// 批量分析任务
|
||||||
|
batchTasks: BatchAnalysisTask[];
|
||||||
|
currentTask: BatchAnalysisTask | null;
|
||||||
|
isBatchAnalyzing: boolean;
|
||||||
|
|
||||||
|
// UI状态
|
||||||
|
activeTab: string;
|
||||||
|
isAnalyzing: boolean;
|
||||||
|
showWeightDialog: boolean;
|
||||||
|
showFactorDialog: boolean;
|
||||||
|
showCompareDialog: boolean;
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
filters: {
|
||||||
|
scoreRange: [number, number];
|
||||||
|
soilTypes: string[];
|
||||||
|
riskLevels: string[];
|
||||||
|
cropCategories: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图表数据
|
||||||
|
chartData: {
|
||||||
|
scoreDistribution: Array<{ range: string; count: number }>;
|
||||||
|
factorPerformance: Array<{ factor: string; score: number; weight: number }>;
|
||||||
|
cropComparison: Array<{ crop: string; suitability: number; economic: number; risk: string }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动作类型
|
||||||
|
export type SpatialAnalysisAction =
|
||||||
|
| { type: 'SET_LAND_BLOCKS'; payload: LandBlock[] }
|
||||||
|
| { type: 'ADD_LAND_BLOCK'; payload: LandBlock }
|
||||||
|
| { type: 'UPDATE_LAND_BLOCK'; payload: { id: string; updates: Partial<LandBlock> } }
|
||||||
|
| { type: 'DELETE_LAND_BLOCK'; payload: string }
|
||||||
|
| { type: 'SET_SELECTED_BLOCKS'; payload: string[] }
|
||||||
|
| { type: 'TOGGLE_BLOCK_SELECTION'; payload: string }
|
||||||
|
|
||||||
|
| { type: 'SET_FACTORS'; payload: EvaluationFactor[] }
|
||||||
|
| { type: 'UPDATE_FACTOR_WEIGHT'; payload: { id: string; weight: number } }
|
||||||
|
| { type: 'TOGGLE_FACTOR'; payload: string }
|
||||||
|
| { type: 'SET_WEIGHT_CONFIG'; payload: WeightConfig }
|
||||||
|
|
||||||
|
| { type: 'SET_CROP_DATABASE'; payload: CropInfo[] }
|
||||||
|
| { type: 'SET_SELECTED_CROPS'; payload: string[] }
|
||||||
|
|
||||||
|
| { type: 'SET_ANALYSIS_RESULTS'; payload: AnalysisResult[] }
|
||||||
|
| { type: 'ADD_ANALYSIS_RESULT'; payload: AnalysisResult }
|
||||||
|
| { type: 'SET_CURRENT_REPORT'; payload: AnalysisReport | null }
|
||||||
|
|
||||||
|
| { type: 'SET_ACTIVE_TAB'; payload: string }
|
||||||
|
| { type: 'SET_ANALYZING'; payload: boolean }
|
||||||
|
| { type: 'TOGGLE_WEIGHT_DIALOG' }
|
||||||
|
| { type: 'TOGGLE_FACTOR_DIALOG' }
|
||||||
|
| { type: 'TOGGLE_COMPARE_DIALOG' }
|
||||||
|
|
||||||
|
| { type: 'SET_FILTERS'; payload: Partial<SpatialAnalysisState['filters']> }
|
||||||
|
| { type: 'RESET_FILTERS' }
|
||||||
|
|
||||||
|
| { type: 'SET_CHART_DATA'; payload: Partial<SpatialAnalysisState['chartData']> }
|
||||||
|
|
||||||
|
| { type: 'RUN_ANALYSIS'; payload: { blockIds?: string[] } }
|
||||||
|
| { type: 'GENERATE_REPORT'; payload: { title: string } }
|
||||||
|
|
||||||
|
// 批量分析相关动作
|
||||||
|
| { type: 'START_BATCH_ANALYSIS'; payload: { name: string; blockIds: string[] } }
|
||||||
|
| { type: 'UPDATE_BATCH_TASK'; payload: { taskId: string; updates: Partial<BatchAnalysisTask> } }
|
||||||
|
| { type: 'SET_CURRENT_TASK'; payload: BatchAnalysisTask | null }
|
||||||
|
| { type: 'COMPLETE_BATCH_ANALYSIS'; payload: { taskId: string; results: AnalysisResult[] } }
|
||||||
|
| { type: 'PAUSE_BATCH_ANALYSIS'; payload: { taskId: string } }
|
||||||
|
| { type: 'RESUME_BATCH_ANALYSIS'; payload: { taskId: string } }
|
||||||
|
| { type: 'CANCEL_BATCH_ANALYSIS'; payload: { taskId: string } }
|
||||||
|
| { type: 'UPDATE_BLOCK_ANALYSIS_STATUS'; payload: { blockId: string; status: LandBlock['analysisStatus']; suitabilityIndex?: number } }
|
||||||
|
|
||||||
|
| { type: 'LOAD_FROM_STORAGE' }
|
||||||
|
| { type: 'SAVE_TO_STORAGE' }
|
||||||
|
| { type: 'RESET_STATE' };
|
||||||
|
|
||||||
|
// 默认评价因子
|
||||||
|
const defaultFactors: EvaluationFactor[] = [
|
||||||
|
{
|
||||||
|
id: 'soil_ph',
|
||||||
|
name: '土壤pH值',
|
||||||
|
weight: 0.15,
|
||||||
|
enabled: true,
|
||||||
|
category: 'soil',
|
||||||
|
description: '土壤酸碱度对作物生长的影响'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soil_organic_matter',
|
||||||
|
name: '有机质含量',
|
||||||
|
weight: 0.12,
|
||||||
|
enabled: true,
|
||||||
|
category: 'soil',
|
||||||
|
description: '土壤肥力的重要指标'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soil_nutrients',
|
||||||
|
name: '土壤养分',
|
||||||
|
weight: 0.18,
|
||||||
|
enabled: true,
|
||||||
|
category: 'soil',
|
||||||
|
description: '氮磷钾等营养元素含量'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soil_texture',
|
||||||
|
name: '土壤质地',
|
||||||
|
weight: 0.10,
|
||||||
|
enabled: true,
|
||||||
|
category: 'soil',
|
||||||
|
description: '土壤物理结构和透气性'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'temperature',
|
||||||
|
name: '温度条件',
|
||||||
|
weight: 0.15,
|
||||||
|
enabled: true,
|
||||||
|
category: 'climate',
|
||||||
|
description: '积温和温度适宜性'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rainfall',
|
||||||
|
name: '降水条件',
|
||||||
|
weight: 0.12,
|
||||||
|
enabled: true,
|
||||||
|
category: 'climate',
|
||||||
|
description: '降雨量及其分布'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sunlight',
|
||||||
|
name: '光照条件',
|
||||||
|
weight: 0.10,
|
||||||
|
enabled: true,
|
||||||
|
category: 'climate',
|
||||||
|
description: '日照时数和光照强度'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'elevation',
|
||||||
|
name: '海拔高度',
|
||||||
|
weight: 0.08,
|
||||||
|
enabled: true,
|
||||||
|
category: 'topography',
|
||||||
|
description: '海拔对气候和作物的影响'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'slope',
|
||||||
|
name: '坡度坡向',
|
||||||
|
weight: 0.06,
|
||||||
|
enabled: true,
|
||||||
|
category: 'topography',
|
||||||
|
description: '地形对水土保持的影响'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'irrigation',
|
||||||
|
name: '灌溉条件',
|
||||||
|
weight: 0.12,
|
||||||
|
enabled: true,
|
||||||
|
category: 'infrastructure',
|
||||||
|
description: '水利设施和灌溉保障'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'accessibility',
|
||||||
|
name: '交通便利性',
|
||||||
|
weight: 0.05,
|
||||||
|
enabled: true,
|
||||||
|
category: 'infrastructure',
|
||||||
|
description: '道路和运输条件'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 默认权重配置
|
||||||
|
const defaultWeightConfig: WeightConfig = {
|
||||||
|
soil: 0.35,
|
||||||
|
climate: 0.30,
|
||||||
|
topography: 0.20,
|
||||||
|
infrastructure: 0.15
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
export const initialSpatialAnalysisState: SpatialAnalysisState = {
|
||||||
|
landBlocks: [],
|
||||||
|
selectedBlocks: [],
|
||||||
|
factors: defaultFactors,
|
||||||
|
weightConfig: defaultWeightConfig,
|
||||||
|
cropDatabase: [],
|
||||||
|
selectedCrops: [],
|
||||||
|
analysisResults: [],
|
||||||
|
currentReport: null,
|
||||||
|
batchTasks: [],
|
||||||
|
currentTask: null,
|
||||||
|
isBatchAnalyzing: false,
|
||||||
|
activeTab: 'overview',
|
||||||
|
isAnalyzing: false,
|
||||||
|
showWeightDialog: false,
|
||||||
|
showFactorDialog: false,
|
||||||
|
showCompareDialog: false,
|
||||||
|
filters: {
|
||||||
|
scoreRange: [0, 100],
|
||||||
|
soilTypes: [],
|
||||||
|
riskLevels: [],
|
||||||
|
cropCategories: []
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
scoreDistribution: [],
|
||||||
|
factorPerformance: [],
|
||||||
|
cropComparison: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
export function spatialAnalysisReducer(
|
||||||
|
state: SpatialAnalysisState,
|
||||||
|
action: SpatialAnalysisAction
|
||||||
|
): SpatialAnalysisState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_LAND_BLOCKS':
|
||||||
|
return { ...state, landBlocks: action.payload };
|
||||||
|
|
||||||
|
case 'ADD_LAND_BLOCK':
|
||||||
|
return { ...state, landBlocks: [...state.landBlocks, action.payload] };
|
||||||
|
|
||||||
|
case 'UPDATE_LAND_BLOCK':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
landBlocks: state.landBlocks.map(block =>
|
||||||
|
block.id === action.payload.id
|
||||||
|
? { ...block, ...action.payload.updates }
|
||||||
|
: block
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'DELETE_LAND_BLOCK':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
landBlocks: state.landBlocks.filter(block => block.id !== action.payload),
|
||||||
|
selectedBlocks: state.selectedBlocks.filter(id => id !== action.payload)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_SELECTED_BLOCKS':
|
||||||
|
return { ...state, selectedBlocks: action.payload };
|
||||||
|
|
||||||
|
case 'TOGGLE_BLOCK_SELECTION':
|
||||||
|
const isSelected = state.selectedBlocks.includes(action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedBlocks: isSelected
|
||||||
|
? state.selectedBlocks.filter(id => id !== action.payload)
|
||||||
|
: [...state.selectedBlocks, action.payload]
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_FACTORS':
|
||||||
|
return { ...state, factors: action.payload };
|
||||||
|
|
||||||
|
case 'UPDATE_FACTOR_WEIGHT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
factors: state.factors.map(factor =>
|
||||||
|
factor.id === action.payload.id
|
||||||
|
? { ...factor, weight: action.payload.weight }
|
||||||
|
: factor
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'TOGGLE_FACTOR':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
factors: state.factors.map(factor =>
|
||||||
|
factor.id === action.payload
|
||||||
|
? { ...factor, enabled: !factor.enabled }
|
||||||
|
: factor
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_WEIGHT_CONFIG':
|
||||||
|
return { ...state, weightConfig: action.payload };
|
||||||
|
|
||||||
|
case 'SET_CROP_DATABASE':
|
||||||
|
return { ...state, cropDatabase: action.payload };
|
||||||
|
|
||||||
|
case 'SET_SELECTED_CROPS':
|
||||||
|
return { ...state, selectedCrops: action.payload };
|
||||||
|
|
||||||
|
case 'SET_ANALYSIS_RESULTS':
|
||||||
|
return { ...state, analysisResults: action.payload };
|
||||||
|
|
||||||
|
case 'ADD_ANALYSIS_RESULT':
|
||||||
|
return { ...state, analysisResults: [...state.analysisResults, action.payload] };
|
||||||
|
|
||||||
|
case 'SET_CURRENT_REPORT':
|
||||||
|
return { ...state, currentReport: action.payload };
|
||||||
|
|
||||||
|
case 'SET_ACTIVE_TAB':
|
||||||
|
return { ...state, activeTab: action.payload };
|
||||||
|
|
||||||
|
case 'SET_ANALYZING':
|
||||||
|
return { ...state, isAnalyzing: action.payload };
|
||||||
|
|
||||||
|
case 'TOGGLE_WEIGHT_DIALOG':
|
||||||
|
return { ...state, showWeightDialog: !state.showWeightDialog };
|
||||||
|
|
||||||
|
case 'TOGGLE_FACTOR_DIALOG':
|
||||||
|
return { ...state, showFactorDialog: !state.showFactorDialog };
|
||||||
|
|
||||||
|
case 'TOGGLE_COMPARE_DIALOG':
|
||||||
|
return { ...state, showCompareDialog: !state.showCompareDialog };
|
||||||
|
|
||||||
|
case 'SET_FILTERS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: { ...state.filters, ...action.payload }
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'RESET_FILTERS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: {
|
||||||
|
scoreRange: [0, 100],
|
||||||
|
soilTypes: [],
|
||||||
|
riskLevels: [],
|
||||||
|
cropCategories: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_CHART_DATA':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
chartData: { ...state.chartData, ...action.payload }
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'RUN_ANALYSIS':
|
||||||
|
// 这里会触发分析逻辑,实际实现在组件中处理
|
||||||
|
return { ...state, isAnalyzing: true };
|
||||||
|
|
||||||
|
case 'GENERATE_REPORT':
|
||||||
|
// 报告生成逻辑在组件中处理
|
||||||
|
return state;
|
||||||
|
|
||||||
|
// 批量分析相关处理
|
||||||
|
case 'START_BATCH_ANALYSIS':
|
||||||
|
const newTask: BatchAnalysisTask = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: action.payload.name,
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date().toLocaleString('zh-CN'),
|
||||||
|
totalBlocks: action.payload.blockIds.length,
|
||||||
|
processedBlocks: 0,
|
||||||
|
successCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
selectedBlockIds: action.payload.blockIds,
|
||||||
|
weightConfig: state.weightConfig,
|
||||||
|
factorIds: state.factors.filter(f => f.enabled).map(f => f.id),
|
||||||
|
progress: 0
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
batchTasks: [...state.batchTasks, newTask],
|
||||||
|
currentTask: newTask,
|
||||||
|
isBatchAnalyzing: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_BATCH_TASK':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
batchTasks: state.batchTasks.map(task =>
|
||||||
|
task.id === action.payload.taskId
|
||||||
|
? { ...task, ...action.payload.updates }
|
||||||
|
: task
|
||||||
|
),
|
||||||
|
currentTask: state.currentTask?.id === action.payload.taskId
|
||||||
|
? { ...state.currentTask, ...action.payload.updates }
|
||||||
|
: state.currentTask
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_CURRENT_TASK':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentTask: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'COMPLETE_BATCH_ANALYSIS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
batchTasks: state.batchTasks.map(task =>
|
||||||
|
task.id === action.payload.taskId
|
||||||
|
? {
|
||||||
|
...task,
|
||||||
|
status: 'completed',
|
||||||
|
completedAt: new Date().toLocaleString('zh-CN'),
|
||||||
|
progress: 100
|
||||||
|
}
|
||||||
|
: task
|
||||||
|
),
|
||||||
|
analysisResults: action.payload.results,
|
||||||
|
isBatchAnalyzing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'PAUSE_BATCH_ANALYSIS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
batchTasks: state.batchTasks.map(task =>
|
||||||
|
task.id === action.payload.taskId
|
||||||
|
? { ...task, status: 'paused' }
|
||||||
|
: task
|
||||||
|
),
|
||||||
|
isBatchAnalyzing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'RESUME_BATCH_ANALYSIS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
batchTasks: state.batchTasks.map(task =>
|
||||||
|
task.id === action.payload.taskId
|
||||||
|
? { ...task, status: 'running' }
|
||||||
|
: task
|
||||||
|
),
|
||||||
|
isBatchAnalyzing: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'CANCEL_BATCH_ANALYSIS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
batchTasks: state.batchTasks.map(task =>
|
||||||
|
task.id === action.payload.taskId
|
||||||
|
? { ...task, status: 'failed', errorMessage: '用户取消分析' }
|
||||||
|
: task
|
||||||
|
),
|
||||||
|
currentTask: null,
|
||||||
|
isBatchAnalyzing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_BLOCK_ANALYSIS_STATUS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
landBlocks: state.landBlocks.map(block =>
|
||||||
|
block.id === action.payload.blockId
|
||||||
|
? {
|
||||||
|
...block,
|
||||||
|
analysisStatus: action.payload.status,
|
||||||
|
...(action.payload.suitabilityIndex && { suitabilityIndex: action.payload.suitabilityIndex }),
|
||||||
|
lastAnalyzed: action.payload.status === 'completed' ? new Date().toLocaleString('zh-CN') : block.lastAnalyzed
|
||||||
|
}
|
||||||
|
: block
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'LOAD_FROM_STORAGE':
|
||||||
|
try {
|
||||||
|
const savedData = localStorage.getItem('spatial-analysis-data');
|
||||||
|
if (savedData) {
|
||||||
|
const parsed = JSON.parse(savedData);
|
||||||
|
return { ...state, ...parsed };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load spatial analysis data:', error);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case 'SAVE_TO_STORAGE':
|
||||||
|
try {
|
||||||
|
const dataToSave = {
|
||||||
|
landBlocks: state.landBlocks,
|
||||||
|
selectedBlocks: state.selectedBlocks,
|
||||||
|
factors: state.factors,
|
||||||
|
weightConfig: state.weightConfig,
|
||||||
|
analysisResults: state.analysisResults,
|
||||||
|
filters: state.filters
|
||||||
|
};
|
||||||
|
localStorage.setItem('spatial-analysis-data', JSON.stringify(dataToSave));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save spatial analysis data:', error);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case 'RESET_STATE':
|
||||||
|
return initialSpatialAnalysisState;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer, useEffect } from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { spatialAnalysisReducer, initialSpatialAnalysisState, SpatialAnalysisState } from './components/spatialAnalysisReducer';
|
||||||
|
import SpatialAnalysisContent from './components/SpatialAnalysisContent';
|
||||||
|
|
||||||
|
export default function BatchEvaluationPage() {
|
||||||
|
const [state, dispatch] = useReducer(spatialAnalysisReducer, initialSpatialAnalysisState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 加载存储的数据
|
||||||
|
dispatch({ type: 'LOAD_FROM_STORAGE' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 保存数据到localStorage
|
||||||
|
dispatch({ type: 'SAVE_TO_STORAGE' });
|
||||||
|
}, [state.factors, state.weightConfig, state.analysisResults, state.batchTasks]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<SpatialAnalysisContent state={state} dispatch={dispatch} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function BatchPage() {
|
|
||||||
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> /land-information/suitability/batch
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function ComprehensivePage() {
|
|
||||||
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> /land-information/suitability/comprehensive
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function CropPage() {
|
|
||||||
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> /land-information/suitability/crop
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,700 @@
|
|||||||
|
/**
|
||||||
|
* 多因子综合评价组件
|
||||||
|
* 提供地块适宜性评价、权重配置、批量分析和作物推荐功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Leaf,
|
||||||
|
TrendingUp,
|
||||||
|
Award,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle2,
|
||||||
|
Play,
|
||||||
|
Settings,
|
||||||
|
Download,
|
||||||
|
Eye,
|
||||||
|
Calculator,
|
||||||
|
Database,
|
||||||
|
RefreshCw,
|
||||||
|
Zap,
|
||||||
|
Target,
|
||||||
|
Droplet,
|
||||||
|
Cloud,
|
||||||
|
Sun,
|
||||||
|
ThermometerSun,
|
||||||
|
BookOpen,
|
||||||
|
Beaker,
|
||||||
|
Info,
|
||||||
|
BarChart3,
|
||||||
|
Filter
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EvaluationFactor,
|
||||||
|
SuitabilityResult,
|
||||||
|
FactorWeight,
|
||||||
|
BatchProgress,
|
||||||
|
getGradeColor,
|
||||||
|
getScoreColor,
|
||||||
|
getSuitabilityLevelColor,
|
||||||
|
formatDate,
|
||||||
|
MOCK_FIELDS
|
||||||
|
} from './multiFactorTypes';
|
||||||
|
import {
|
||||||
|
MultiFactorService
|
||||||
|
} from './multiFactorService';
|
||||||
|
import {
|
||||||
|
matchCropsForField,
|
||||||
|
cropKnowledgeBase
|
||||||
|
} from './cropKnowledgeBase';
|
||||||
|
|
||||||
|
export function MultiFactorEvaluation() {
|
||||||
|
const [selectedField, setSelectedField] = useState('field-1');
|
||||||
|
const [showWeightConfig, setShowWeightConfig] = useState(false);
|
||||||
|
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
|
||||||
|
const [batchProgress, setBatchProgress] = useState<BatchProgress>({
|
||||||
|
total: 0,
|
||||||
|
processed: 0,
|
||||||
|
highSuitability: 0,
|
||||||
|
mediumSuitability: 0,
|
||||||
|
lowSuitability: 0,
|
||||||
|
currentField: '',
|
||||||
|
});
|
||||||
|
const [isBatchRunning, setIsBatchRunning] = useState(false);
|
||||||
|
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
|
||||||
|
|
||||||
|
// 评价因子权重配置
|
||||||
|
const [factorWeights, setFactorWeights] = useState<FactorWeight[]>([
|
||||||
|
{ id: 'ph', name: 'pH值', weight: 20, unit: '' },
|
||||||
|
{ id: 'organic', name: '有机质含量', weight: 25, unit: 'g/kg' },
|
||||||
|
{ id: 'depth', name: '土层厚度', weight: 20, unit: 'cm' },
|
||||||
|
{ id: 'nitrogen', name: '全氮', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'phosphorus', name: '全磷', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'potassium', name: '全钾', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'drainage', name: '排水性', weight: 5, unit: '' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 模拟适宜性评价结果
|
||||||
|
const [evaluationResults, setEvaluationResults] = useState<SuitabilityResult[]>(
|
||||||
|
MultiFactorService.generateMockEvaluationData()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取当前选中的地块结果
|
||||||
|
const currentResult =
|
||||||
|
evaluationResults.find(r => r.fieldId === selectedField) ||
|
||||||
|
batchAnalysisResults.find(r => r.fieldId === selectedField) ||
|
||||||
|
evaluationResults[0];
|
||||||
|
|
||||||
|
// 批量分析处理函数
|
||||||
|
const handleRunBatchAnalysis = async () => {
|
||||||
|
const validation = MultiFactorService.validateWeights(factorWeights);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
toast.error(`权重总和必须为100%才能进行批量分析(当前:${validation.totalWeight}%)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsBatchRunning(true);
|
||||||
|
setBatchProgress({
|
||||||
|
total: 68,
|
||||||
|
processed: 0,
|
||||||
|
highSuitability: 0,
|
||||||
|
mediumSuitability: 0,
|
||||||
|
lowSuitability: 0,
|
||||||
|
currentField: '',
|
||||||
|
});
|
||||||
|
setBatchAnalysisResults([]);
|
||||||
|
|
||||||
|
toast.success('开始批量分析,正在读取地块数据...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await MultiFactorService.runBatchAnalysis(
|
||||||
|
factorWeights,
|
||||||
|
(progress) => {
|
||||||
|
setBatchProgress(progress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setBatchAnalysisResults(results);
|
||||||
|
setIsBatchRunning(false);
|
||||||
|
toast.success(`批量分析完成!已为${results.length}个地块生成适宜性评价结果`);
|
||||||
|
} catch (error) {
|
||||||
|
setIsBatchRunning(false);
|
||||||
|
toast.error('批量分析失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateWeight = (id: string, newWeight: number) => {
|
||||||
|
setFactorWeights(prev =>
|
||||||
|
MultiFactorService.updateWeight(prev, id, newWeight)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetWeights = () => {
|
||||||
|
setFactorWeights(MultiFactorService.resetWeights());
|
||||||
|
toast.success('权重已恢复默认值');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplyPreset = (preset: 'grain' | 'economic' | 'default') => {
|
||||||
|
setFactorWeights(MultiFactorService.getWeightPreset(preset));
|
||||||
|
const presetName = preset === 'grain' ? '粮食作物' : preset === 'economic' ? '经济作物' : '默认';
|
||||||
|
toast.success(`已应用${presetName}权重方案`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
|
||||||
|
|
||||||
|
// 执行地块适宜性评价
|
||||||
|
const handleEvaluateField = () => {
|
||||||
|
const validation = MultiFactorService.validateWeights(factorWeights);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
toast.error(`权重总和必须为100%才能进行评价(当前:${validation.totalWeight}%)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = MultiFactorService.generateEvaluationResult(selectedField, factorWeights);
|
||||||
|
setEvaluationResults(prev =>
|
||||||
|
prev.map(r => r.fieldId === selectedField ? result : r)
|
||||||
|
);
|
||||||
|
toast.success('评价完成!已应用当前权重配置计算综合得分');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('评价失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportResults = () => {
|
||||||
|
const resultsToExport = batchAnalysisResults.length > 0 ? batchAnalysisResults : evaluationResults;
|
||||||
|
toast.success('正在导出评价结果...');
|
||||||
|
// 模拟导出功能
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.success(`已导出${resultsToExport.length}个地块的评价结果`);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-green-800 dark:text-green-200">多因子综合评价</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
地块适宜性评价、自动化空间分析与作物适配推荐
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setShowKnowledgeBase(true)}>
|
||||||
|
<BookOpen className="w-4 h-4 mr-2" />
|
||||||
|
作物知识库
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => setShowWeightConfig(true)}>
|
||||||
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
|
权重配置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 多因子综合评价 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">选择地块</label>
|
||||||
|
<Select value={selectedField} onValueChange={setSelectedField}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{evaluationResults.map((result) => (
|
||||||
|
<SelectItem key={result.fieldId} value={result.fieldId}>
|
||||||
|
{result.fieldName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 items-end">
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={handleEvaluateField}
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
开始评价
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 评价结果总览 */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<Card className="p-6 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900">
|
||||||
|
<div className="text-center">
|
||||||
|
<Award className="w-12 h-12 text-green-600 dark:text-green-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">综合评分</p>
|
||||||
|
<p
|
||||||
|
className="text-4xl mb-2"
|
||||||
|
style={{ color: getGradeColor(currentResult.grade) }}
|
||||||
|
>
|
||||||
|
{currentResult.totalScore}
|
||||||
|
</p>
|
||||||
|
<Badge
|
||||||
|
className="text-white font-light"
|
||||||
|
style={{ backgroundColor: getGradeColor(currentResult.grade) }}
|
||||||
|
>
|
||||||
|
{currentResult.grade}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<CheckCircle2 className="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">优秀因子</p>
|
||||||
|
<p className="text-4xl text-blue-600 dark:text-blue-400 mb-2">
|
||||||
|
{currentResult.factors.filter(f => f.score >= 80).length}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
/ {currentResult.factors.length} 项
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<AlertTriangle className="w-12 h-12 text-yellow-600 dark:text-yellow-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">待改善因子</p>
|
||||||
|
<p className="text-4xl text-yellow-600 dark:text-yellow-400 mb-2">
|
||||||
|
{currentResult.factors.filter(f => f.score < 70).length}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">需要优化</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<TrendingUp className="w-12 h-12 text-purple-600 dark:text-purple-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">评价时间</p>
|
||||||
|
<p className="text-sm text-purple-600 dark:text-purple-400 mb-2">
|
||||||
|
{formatDate(currentResult.timestamp)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">最近更新</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 因子详细评分 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">评价因子详细分析</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentResult.factors.map((factor) => (
|
||||||
|
<div key={factor.id} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm font-medium">{factor.name}</span>
|
||||||
|
<Badge variant="outline" className="text-xs font-light">
|
||||||
|
权重: {factor.weight}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
实际值: {factor.value.toFixed(1)}{factor.unit}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
最佳范围: {factor.optimalRange[0]}-{factor.optimalRange[1]}{factor.unit}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-sm font-medium"
|
||||||
|
style={{ color: getScoreColor(factor.score) }}
|
||||||
|
>
|
||||||
|
{factor.score}分
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Progress value={factor.score} className="h-2" />
|
||||||
|
<div className="absolute top-0 left-0 h-2 w-full flex items-center">
|
||||||
|
<div
|
||||||
|
className="absolute h-3 w-1 bg-blue-500"
|
||||||
|
style={{
|
||||||
|
left: `${((factor.optimalRange[0] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute h-3 w-1 bg-blue-500"
|
||||||
|
style={{
|
||||||
|
left: `${((factor.optimalRange[1] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 加权计算说明 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">层次分析法(AHP)加权计算</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-900 dark:text-blue-100 mb-2">计算公式:</p>
|
||||||
|
<code className="text-xs text-blue-800 dark:text-blue-200 block mb-2">
|
||||||
|
总分 = Σ(因子得分 × 因子权重)
|
||||||
|
</code>
|
||||||
|
<p className="text-xs text-blue-800 dark:text-blue-200">
|
||||||
|
= ({currentResult.factors.map((f, i) =>
|
||||||
|
`${f.score} × ${f.weight}%${i < currentResult.factors.length - 1 ? ' + ' : ''}`
|
||||||
|
).join('')})
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-blue-800 dark:text-blue-200 mt-2">
|
||||||
|
= {currentResult.totalScore}分
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<h4 className="text-green-900 dark:text-green-100 mb-2">高度适宜 (≥80分)</h4>
|
||||||
|
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
|
||||||
|
<li>• 土壤条件优秀</li>
|
||||||
|
<li>• 适合多种作物</li>
|
||||||
|
<li>• 预期产量高</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
|
||||||
|
<h4 className="text-yellow-900 dark:text-yellow-100 mb-2">一般适宜 (60-79分)</h4>
|
||||||
|
<ul className="text-sm text-yellow-800 dark:text-yellow-200 space-y-1">
|
||||||
|
<li>• 部分因子需改善</li>
|
||||||
|
<li>• 可种植耐性作物</li>
|
||||||
|
<li>• 需适当改良</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-950 rounded-lg">
|
||||||
|
<h4 className="text-red-900 dark:text-red-100 mb-2">不适宜 (<60分)</h4>
|
||||||
|
<ul className="text-sm text-red-800 dark:text-red-200 space-y-1">
|
||||||
|
<li>• 土壤条件较差</li>
|
||||||
|
<li>• 需大幅改良</li>
|
||||||
|
<li>• 风险较高</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 多因子综合评价功能说明 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Zap className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="mb-2">多因子综合评价功能说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>关键指标整合</strong>: 整合pH值、有机质含量、土层厚度、全氮、全磷、全钾、排水性等7项关键土壤指标</li>
|
||||||
|
<li>• <strong>层次分析法(AHP)</strong>: 采用层次分析法构建加权评分体系,公式:总分 = Σ(因子得分 × 因子权重)</li>
|
||||||
|
<li>• <strong>单因子评分</strong>: 根据实际值与最佳范围的接近程度计算各因子得分(0-100分),范围内得分85-100分</li>
|
||||||
|
<li>• <strong>自定义权重</strong>: 支持手动调整各指标权重,提供粮食作物和经济作物两种预设方案</li>
|
||||||
|
<li>• <strong>综合得分</strong>: 输出0-100分的适宜性评分,自动计算加权总分</li>
|
||||||
|
<li>• <strong>三级分级</strong>: 高度适宜(≥80分)、一般适宜(60-79分)、不适宜(<60分)</li>
|
||||||
|
<li>• <strong>可视化展示</strong>: 提供进度条、颜色标识等多维度可视化,直观展示各因子评分与最佳范围对比</li>
|
||||||
|
<li>• <strong>改善建议</strong>: 自动识别待改善因子,为土壤改良提供决策依据</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重配置对话框 */}
|
||||||
|
<Dialog open={showWeightConfig} onOpenChange={setShowWeightConfig}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>评价因子权重配置</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
调整各因子的权重百分比以适应不同的评价需求
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 预设方案 */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3">预设权重方案</h4>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleApplyPreset('default')}
|
||||||
|
>
|
||||||
|
默认配置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleApplyPreset('grain')}
|
||||||
|
>
|
||||||
|
粮食作物
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleApplyPreset('economic')}
|
||||||
|
>
|
||||||
|
经济作物
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleResetWeights}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重调整 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="text-sm font-medium">因子权重调整</h4>
|
||||||
|
<span className={`text-sm font-medium ${
|
||||||
|
totalWeight === 100 ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}>
|
||||||
|
总计: {totalWeight}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{factorWeights.map((factor) => (
|
||||||
|
<div key={factor.id} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">{factor.name}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={factor.weight}
|
||||||
|
onChange={(e) => handleUpdateWeight(factor.id, parseInt(e.target.value) || 0)}
|
||||||
|
className="w-16 h-8 text-xs"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[factor.weight]}
|
||||||
|
onValueChange={([value]) => handleUpdateWeight(factor.id, value)}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
className="h-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalWeight !== 100 && (
|
||||||
|
<div className="p-3 bg-red-50 dark:bg-red-950 rounded-lg">
|
||||||
|
<p className="text-sm text-red-800 dark:text-red-200">
|
||||||
|
⚠️ 权重总和必须为100%(当前:{totalWeight}%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 权重说明 */}
|
||||||
|
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">权重配置说明</h4>
|
||||||
|
<div className="space-y-2 text-xs text-blue-800 dark:text-blue-200">
|
||||||
|
<p>• <strong>权重总和</strong>: 所有因子权重总和必须为100%</p>
|
||||||
|
<p>• <strong>权重影响</strong>: 权重越高的因子对最终得分影响越大</p>
|
||||||
|
<p>• <strong>预设方案</strong>: 系统提供针对不同作物类型的优化权重配置</p>
|
||||||
|
<p>• <strong>自定义配置</strong>: 可根据具体需求手动调整各因子权重</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 作物知识库对话框 */}
|
||||||
|
<Dialog open={showKnowledgeBase} onOpenChange={setShowKnowledgeBase}>
|
||||||
|
<DialogContent className="max-w-6xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>作物知识库</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
查看系统中支持的作物品种及其生长条件要求
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 知识库统计 */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400 mb-1">
|
||||||
|
{cropKnowledgeBase.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">作物品种</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600 dark:text-green-400 mb-1">
|
||||||
|
{new Set(cropKnowledgeBase.map(crop => crop.category)).size}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">作物类别</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400 mb-1">
|
||||||
|
{cropKnowledgeBase.reduce((sum, crop) => sum + crop.riskFactors.length, 0)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">风险因子</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 作物列表 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{cropKnowledgeBase.map((crop) => (
|
||||||
|
<Card key={crop.id} className="p-4 bg-card">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<h4 className="text-base font-medium">{crop.cropName}</h4>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{crop.category}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{crop.growthCycle.days}天
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{crop.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* 土壤要求 */}
|
||||||
|
<div>
|
||||||
|
<h5 className="text-sm font-medium mb-2 flex items-center gap-2">
|
||||||
|
<Beaker className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
土壤要求
|
||||||
|
</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>pH值</span>
|
||||||
|
<span>{crop.soilRequirements.ph.optimal[0]}-{crop.soilRequirements.ph.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>有机质 (g/kg)</span>
|
||||||
|
<span>{crop.soilRequirements.organicMatter.optimal[0]}-{crop.soilRequirements.organicMatter.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>土层厚度 (cm)</span>
|
||||||
|
<span>{crop.soilRequirements.soilDepth.optimal[0]}-{crop.soilRequirements.soilDepth.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>全氮 (g/kg)</span>
|
||||||
|
<span>{crop.soilRequirements.nitrogen.optimal[0]}-{crop.soilRequirements.nitrogen.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>全磷 (g/kg)</span>
|
||||||
|
<span>{crop.soilRequirements.phosphorus.optimal[0]}-{crop.soilRequirements.phosphorus.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>全钾 (g/kg)</span>
|
||||||
|
<span>{crop.soilRequirements.potassium.optimal[0]}-{crop.soilRequirements.potassium.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 气候要求 */}
|
||||||
|
<div>
|
||||||
|
<h5 className="text-sm font-medium mb-2 flex items-center gap-2">
|
||||||
|
<Cloud className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
气候要求
|
||||||
|
</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>温度 (°C)</span>
|
||||||
|
<span>{crop.climateRequirements.temperature.optimal[0]}-{crop.climateRequirements.temperature.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>降雨 (mm/年)</span>
|
||||||
|
<span>{crop.climateRequirements.rainfall.optimal[0]}-{crop.climateRequirements.rainfall.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>光照 (小时/天)</span>
|
||||||
|
<span>{crop.climateRequirements.sunlight.optimal[0]}-{crop.climateRequirements.sunlight.optimal[1]}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 预期产量 */}
|
||||||
|
<div className="mt-3">
|
||||||
|
<h5 className="text-sm font-medium mb-2 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
预期产量 (kg/亩)
|
||||||
|
</h5>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
|
<div className="text-center p-2 bg-green-50 dark:bg-green-950 rounded">
|
||||||
|
<div className="font-medium text-green-700 dark:text-green-300">
|
||||||
|
{crop.expectedYield.high[0]}-{crop.expectedYield.high[1]}
|
||||||
|
</div>
|
||||||
|
<div className="text-green-600 dark:text-green-400">高产</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 bg-blue-50 dark:bg-blue-950 rounded">
|
||||||
|
<div className="font-medium text-blue-700 dark:text-blue-300">
|
||||||
|
{crop.expectedYield.medium[0]}-{crop.expectedYield.medium[1]}
|
||||||
|
</div>
|
||||||
|
<div className="text-blue-600 dark:text-blue-400">中产</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 bg-yellow-50 dark:bg-yellow-950 rounded">
|
||||||
|
<div className="font-medium text-yellow-700 dark:text-yellow-300">
|
||||||
|
{crop.expectedYield.low[0]}-{crop.expectedYield.low[1]}
|
||||||
|
</div>
|
||||||
|
<div className="text-yellow-600 dark:text-yellow-400">低产</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 风险因子 */}
|
||||||
|
{crop.riskFactors.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h5 className="text-sm font-medium mb-2 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||||
|
主要风险因子
|
||||||
|
</h5>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{crop.riskFactors.map((risk) => (
|
||||||
|
<div key={risk.id} className="p-2 bg-red-50 dark:bg-red-950 rounded">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className={`w-2 h-2 rounded-full ${
|
||||||
|
risk.severity === 'high' ? 'bg-red-500' : 'bg-yellow-500'
|
||||||
|
}`} />
|
||||||
|
<span className="text-xs font-medium">{risk.name}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{risk.suggestion}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,565 @@
|
|||||||
|
/**
|
||||||
|
* 作物知识库服务
|
||||||
|
* 提供作物-环境适配数据和分析功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Crop, CropRecommendation, FieldFactors, MatchDetail, RiskFactor } from './multiFactorTypes';
|
||||||
|
|
||||||
|
// 作物知识库数据
|
||||||
|
export const cropKnowledgeBase: Crop[] = [
|
||||||
|
// 粮食作物
|
||||||
|
{
|
||||||
|
id: 'rice',
|
||||||
|
cropName: '水稻',
|
||||||
|
category: '粮食作物',
|
||||||
|
description: '水稻是世界上最重要的粮食作物之一,需要充足的水分和温暖的气候条件。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 120,
|
||||||
|
seasons: ['春季', '夏季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [5.5, 6.5], acceptable: [5.0, 7.0] },
|
||||||
|
organicMatter: { optimal: [25, 40], acceptable: [20, 50] },
|
||||||
|
soilDepth: { optimal: [50, 100], acceptable: [30, 120] },
|
||||||
|
nitrogen: { optimal: [1.5, 2.5], acceptable: [1.0, 3.0] },
|
||||||
|
phosphorus: { optimal: [1.0, 2.0], acceptable: [0.5, 2.5] },
|
||||||
|
potassium: { optimal: [15, 25], acceptable: [10, 30] },
|
||||||
|
drainage: { optimal: [60, 80], acceptable: [50, 90] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [25, 32], acceptable: [20, 35] },
|
||||||
|
rainfall: { optimal: [1000, 1500], acceptable: [800, 2000] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [600, 800],
|
||||||
|
medium: [400, 600],
|
||||||
|
low: [200, 400]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'rice-cold',
|
||||||
|
name: '低温冷害',
|
||||||
|
condition: '温度低于20°C',
|
||||||
|
suggestion: '选择耐寒品种,采用保温育苗技术',
|
||||||
|
severity: 'high',
|
||||||
|
category: 'climate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rice-drought',
|
||||||
|
name: '干旱胁迫',
|
||||||
|
condition: '降雨量低于800mm/年',
|
||||||
|
suggestion: '建设灌溉系统,选用耐旱品种',
|
||||||
|
severity: 'high',
|
||||||
|
category: 'climate'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wheat',
|
||||||
|
cropName: '小麦',
|
||||||
|
category: '粮食作物',
|
||||||
|
description: '小麦是温带地区重要的粮食作物,适应性较强,耐寒性较好。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 110,
|
||||||
|
seasons: ['秋季', '春季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.5], acceptable: [5.5, 8.0] },
|
||||||
|
organicMatter: { optimal: [20, 35], acceptable: [15, 40] },
|
||||||
|
soilDepth: { optimal: [60, 120], acceptable: [40, 150] },
|
||||||
|
nitrogen: { optimal: [1.2, 2.0], acceptable: [0.8, 2.5] },
|
||||||
|
phosphorus: { optimal: [0.8, 1.5], acceptable: [0.5, 2.0] },
|
||||||
|
potassium: { optimal: [12, 20], acceptable: [8, 25] },
|
||||||
|
drainage: { optimal: [70, 90], acceptable: [60, 100] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [15, 22], acceptable: [10, 25] },
|
||||||
|
rainfall: { optimal: [400, 600], acceptable: [300, 800] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [500, 700],
|
||||||
|
medium: [300, 500],
|
||||||
|
low: [150, 300]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'wheat-heat',
|
||||||
|
name: '高温热害',
|
||||||
|
condition: '温度超过25°C',
|
||||||
|
suggestion: '选择耐热品种,调整播种期',
|
||||||
|
severity: 'medium',
|
||||||
|
category: 'climate'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'corn',
|
||||||
|
cropName: '玉米',
|
||||||
|
category: '粮食作物',
|
||||||
|
description: '玉米是重要的粮食和饲料作物,喜温喜光,需肥量大。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 130,
|
||||||
|
seasons: ['春季', '夏季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [25, 40], acceptable: [20, 50] },
|
||||||
|
soilDepth: { optimal: [80, 150], acceptable: [60, 200] },
|
||||||
|
nitrogen: { optimal: [1.8, 2.8], acceptable: [1.2, 3.5] },
|
||||||
|
phosphorus: { optimal: [1.2, 2.2], acceptable: [0.8, 3.0] },
|
||||||
|
potassium: { optimal: [18, 30], acceptable: [12, 40] },
|
||||||
|
drainage: { optimal: [80, 95], acceptable: [70, 100] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [22, 30], acceptable: [18, 32] },
|
||||||
|
rainfall: { optimal: [500, 800], acceptable: [400, 1000] },
|
||||||
|
sunlight: { optimal: [7, 9], acceptable: [6, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [700, 900],
|
||||||
|
medium: [450, 700],
|
||||||
|
low: [250, 450]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'corn-drought',
|
||||||
|
name: '干旱胁迫',
|
||||||
|
condition: '降雨量低于400mm/年',
|
||||||
|
suggestion: '建设灌溉设施,选用耐旱品种',
|
||||||
|
severity: 'high',
|
||||||
|
category: 'climate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'corn-pollination',
|
||||||
|
name: '授粉不良',
|
||||||
|
condition: '开花期温度超过35°C',
|
||||||
|
suggestion: '调整播种期,选择耐热品种',
|
||||||
|
severity: 'medium',
|
||||||
|
category: 'climate'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// 经济作物
|
||||||
|
{
|
||||||
|
id: 'cotton',
|
||||||
|
cropName: '棉花',
|
||||||
|
category: '经济作物',
|
||||||
|
description: '棉花是重要的经济作物,喜温喜光,对土壤要求较高。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 150,
|
||||||
|
seasons: ['春季', '夏季', '秋季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.5, 8.0], acceptable: [6.0, 8.5] },
|
||||||
|
organicMatter: { optimal: [20, 35], acceptable: [15, 40] },
|
||||||
|
soilDepth: { optimal: [100, 180], acceptable: [80, 200] },
|
||||||
|
nitrogen: { optimal: [1.5, 2.5], acceptable: [1.0, 3.0] },
|
||||||
|
phosphorus: { optimal: [1.0, 2.0], acceptable: [0.6, 2.5] },
|
||||||
|
potassium: { optimal: [20, 35], acceptable: [15, 45] },
|
||||||
|
drainage: { optimal: [85, 95], acceptable: [75, 100] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [25, 32], acceptable: [20, 35] },
|
||||||
|
rainfall: { optimal: [400, 600], acceptable: [300, 800] },
|
||||||
|
sunlight: { optimal: [8, 10], acceptable: [7, 12] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [120, 180],
|
||||||
|
medium: [80, 120],
|
||||||
|
low: [40, 80]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'cotton-boll',
|
||||||
|
name: '蕾铃脱落',
|
||||||
|
condition: '湿度过大或温差过大',
|
||||||
|
suggestion: '合理密植,加强田间管理',
|
||||||
|
severity: 'medium',
|
||||||
|
category: 'climate'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soybean',
|
||||||
|
cropName: '大豆',
|
||||||
|
category: '经济作物',
|
||||||
|
description: '大豆是重要的油料作物,固氮能力强,适应性较广。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 100,
|
||||||
|
seasons: ['春季', '夏季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [25, 40], acceptable: [20, 50] },
|
||||||
|
soilDepth: { optimal: [60, 120], acceptable: [40, 150] },
|
||||||
|
nitrogen: { optimal: [1.2, 2.0], acceptable: [0.8, 2.5] },
|
||||||
|
phosphorus: { optimal: [1.0, 2.0], acceptable: [0.6, 2.5] },
|
||||||
|
potassium: { optimal: [15, 25], acceptable: [10, 30] },
|
||||||
|
drainage: { optimal: [75, 90], acceptable: [65, 100] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [20, 28], acceptable: [15, 32] },
|
||||||
|
rainfall: { optimal: [500, 700], acceptable: [400, 900] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [250, 350],
|
||||||
|
medium: [150, 250],
|
||||||
|
low: [80, 150]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'soybean-flower',
|
||||||
|
name: '落花落荚',
|
||||||
|
condition: '高温干旱或连续阴雨',
|
||||||
|
suggestion: '合理施肥,科学管水',
|
||||||
|
severity: 'medium',
|
||||||
|
category: 'climate'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// 蔬菜作物
|
||||||
|
{
|
||||||
|
id: 'tomato',
|
||||||
|
cropName: '番茄',
|
||||||
|
category: '蔬菜作物',
|
||||||
|
description: '番茄是重要的蔬菜作物,喜温喜光,对肥水要求较高。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 90,
|
||||||
|
seasons: ['春季', '夏季', '秋季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [30, 45], acceptable: [25, 50] },
|
||||||
|
soilDepth: { optimal: [40, 80], acceptable: [30, 100] },
|
||||||
|
nitrogen: { optimal: [2.0, 3.0], acceptable: [1.5, 3.5] },
|
||||||
|
phosphorus: { optimal: [1.5, 2.5], acceptable: [1.0, 3.0] },
|
||||||
|
potassium: { optimal: [25, 40], acceptable: [20, 50] },
|
||||||
|
drainage: { optimal: [80, 95], acceptable: [70, 100] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [22, 28], acceptable: [18, 32] },
|
||||||
|
rainfall: { optimal: [400, 600], acceptable: [300, 800] },
|
||||||
|
sunlight: { optimal: [7, 9], acceptable: [6, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [8000, 12000],
|
||||||
|
medium: [5000, 8000],
|
||||||
|
low: [2000, 5000]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'tomato-disease',
|
||||||
|
name: '病害高发',
|
||||||
|
condition: '湿度过大,通风不良',
|
||||||
|
suggestion: '加强病虫害防治,合理密植',
|
||||||
|
severity: 'high',
|
||||||
|
category: 'disease'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cucumber',
|
||||||
|
cropName: '黄瓜',
|
||||||
|
category: '蔬菜作物',
|
||||||
|
description: '黄瓜是重要的蔬菜作物,喜温喜湿,生长周期短。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 60,
|
||||||
|
seasons: ['春季', '夏季', '秋季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [30, 45], acceptable: [25, 50] },
|
||||||
|
soilDepth: { optimal: [30, 60], acceptable: [20, 80] },
|
||||||
|
nitrogen: { optimal: [2.5, 3.5], acceptable: [2.0, 4.0] },
|
||||||
|
phosphorus: { optimal: [1.8, 2.8], acceptable: [1.2, 3.5] },
|
||||||
|
potassium: { optimal: [30, 45], acceptable: [25, 55] },
|
||||||
|
drainage: { optimal: [85, 95], acceptable: [75, 100] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [22, 30], acceptable: [18, 32] },
|
||||||
|
rainfall: { optimal: [500, 700], acceptable: [400, 900] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [6000, 9000],
|
||||||
|
medium: [4000, 6000],
|
||||||
|
low: [2000, 4000]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'cucumber-pest',
|
||||||
|
name: '虫害严重',
|
||||||
|
condition: '温度适宜,湿度大',
|
||||||
|
suggestion: '加强虫害监测,及时防治',
|
||||||
|
severity: 'medium',
|
||||||
|
category: 'pest'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配作物推荐
|
||||||
|
*/
|
||||||
|
export function matchCropsForField(fieldFactors: FieldFactors): CropRecommendation[] {
|
||||||
|
const recommendations: CropRecommendation[] = [];
|
||||||
|
|
||||||
|
for (const crop of cropKnowledgeBase) {
|
||||||
|
const matchDetails: MatchDetail[] = [];
|
||||||
|
let totalScore = 0;
|
||||||
|
let factorCount = 0;
|
||||||
|
|
||||||
|
// 土壤pH匹配
|
||||||
|
const phScore = calculateFactorScore(
|
||||||
|
fieldFactors.ph,
|
||||||
|
crop.soilRequirements.ph.optimal,
|
||||||
|
crop.soilRequirements.ph.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: 'pH',
|
||||||
|
value: fieldFactors.ph,
|
||||||
|
status: getScoreStatus(phScore),
|
||||||
|
score: phScore
|
||||||
|
});
|
||||||
|
totalScore += phScore;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
// 有机质匹配
|
||||||
|
const organicScore = calculateFactorScore(
|
||||||
|
fieldFactors.organic,
|
||||||
|
crop.soilRequirements.organicMatter.optimal,
|
||||||
|
crop.soilRequirements.organicMatter.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '有机质',
|
||||||
|
value: fieldFactors.organic,
|
||||||
|
status: getScoreStatus(organicScore),
|
||||||
|
score: organicScore
|
||||||
|
});
|
||||||
|
totalScore += organicScore;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
// 土层厚度匹配
|
||||||
|
const depthScore = calculateFactorScore(
|
||||||
|
fieldFactors.depth,
|
||||||
|
crop.soilRequirements.soilDepth.optimal,
|
||||||
|
crop.soilRequirements.soilDepth.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '土层厚度',
|
||||||
|
value: fieldFactors.depth,
|
||||||
|
status: getScoreStatus(depthScore),
|
||||||
|
score: depthScore
|
||||||
|
});
|
||||||
|
totalScore += depthScore;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
// 全氮匹配
|
||||||
|
const nitrogenScore = calculateFactorScore(
|
||||||
|
fieldFactors.nitrogen,
|
||||||
|
crop.soilRequirements.nitrogen.optimal,
|
||||||
|
crop.soilRequirements.nitrogen.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '全氮',
|
||||||
|
value: fieldFactors.nitrogen,
|
||||||
|
status: getScoreStatus(nitrogenScore),
|
||||||
|
score: nitrogenScore
|
||||||
|
});
|
||||||
|
totalScore += nitrogenScore;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
// 全磷匹配
|
||||||
|
const phosphorusScore = calculateFactorScore(
|
||||||
|
fieldFactors.phosphorus,
|
||||||
|
crop.soilRequirements.phosphorus.optimal,
|
||||||
|
crop.soilRequirements.phosphorus.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '全磷',
|
||||||
|
value: fieldFactors.phosphorus,
|
||||||
|
status: getScoreStatus(phosphorusScore),
|
||||||
|
score: phosphorusScore
|
||||||
|
});
|
||||||
|
totalScore += phosphorusScore;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
// 全钾匹配
|
||||||
|
const potassiumScore = calculateFactorScore(
|
||||||
|
fieldFactors.potassium,
|
||||||
|
crop.soilRequirements.potassium.optimal,
|
||||||
|
crop.soilRequirements.potassium.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '全钾',
|
||||||
|
value: fieldFactors.potassium,
|
||||||
|
status: getScoreStatus(potassiumScore),
|
||||||
|
score: potassiumScore
|
||||||
|
});
|
||||||
|
totalScore += potassiumScore;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
// 排水性匹配
|
||||||
|
const drainageScore = calculateFactorScore(
|
||||||
|
fieldFactors.drainage,
|
||||||
|
crop.soilRequirements.drainage.optimal,
|
||||||
|
crop.soilRequirements.drainage.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '排水性',
|
||||||
|
value: fieldFactors.drainage,
|
||||||
|
status: getScoreStatus(drainageScore),
|
||||||
|
score: drainageScore
|
||||||
|
});
|
||||||
|
totalScore += drainageScore;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
// 气温匹配(如果有数据)
|
||||||
|
if (fieldFactors.temperature !== undefined) {
|
||||||
|
const tempScore = calculateFactorScore(
|
||||||
|
fieldFactors.temperature,
|
||||||
|
crop.climateRequirements.temperature.optimal,
|
||||||
|
crop.climateRequirements.temperature.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '温度',
|
||||||
|
value: fieldFactors.temperature,
|
||||||
|
status: getScoreStatus(tempScore),
|
||||||
|
score: tempScore
|
||||||
|
});
|
||||||
|
totalScore += tempScore;
|
||||||
|
factorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 降雨匹配(如果有数据)
|
||||||
|
if (fieldFactors.rainfall !== undefined) {
|
||||||
|
const rainScore = calculateFactorScore(
|
||||||
|
fieldFactors.rainfall,
|
||||||
|
crop.climateRequirements.rainfall.optimal,
|
||||||
|
crop.climateRequirements.rainfall.acceptable
|
||||||
|
);
|
||||||
|
matchDetails.push({
|
||||||
|
factor: '降雨',
|
||||||
|
value: fieldFactors.rainfall,
|
||||||
|
status: getScoreStatus(rainScore),
|
||||||
|
score: rainScore
|
||||||
|
});
|
||||||
|
totalScore += rainScore;
|
||||||
|
factorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算综合匹配分数
|
||||||
|
const averageScore = Math.round(totalScore / factorCount);
|
||||||
|
|
||||||
|
// 确定适宜性等级
|
||||||
|
let suitabilityLevel: '高度推荐' | '推荐' | '谨慎种植' | '不推荐';
|
||||||
|
if (averageScore >= 85) {
|
||||||
|
suitabilityLevel = '高度推荐';
|
||||||
|
} else if (averageScore >= 70) {
|
||||||
|
suitabilityLevel = '推荐';
|
||||||
|
} else if (averageScore >= 50) {
|
||||||
|
suitabilityLevel = '谨慎种植';
|
||||||
|
} else {
|
||||||
|
suitabilityLevel = '不推荐';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取适用风险因子
|
||||||
|
const applicableRisks = crop.riskFactors.filter(risk => {
|
||||||
|
if (risk.category === 'climate' && fieldFactors.temperature !== undefined) {
|
||||||
|
return checkRiskCondition(risk.condition, fieldFactors);
|
||||||
|
}
|
||||||
|
// 其他风险因子的判断逻辑
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确定预期产量
|
||||||
|
let expectedYield: [number, number];
|
||||||
|
if (suitabilityLevel === '高度推荐') {
|
||||||
|
expectedYield = crop.expectedYield.high;
|
||||||
|
} else if (suitabilityLevel === '推荐') {
|
||||||
|
expectedYield = crop.expectedYield.medium;
|
||||||
|
} else {
|
||||||
|
expectedYield = crop.expectedYield.low;
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendations.push({
|
||||||
|
crop,
|
||||||
|
matchScore: averageScore,
|
||||||
|
suitabilityLevel,
|
||||||
|
matchDetails,
|
||||||
|
applicableRisks,
|
||||||
|
expectedYield
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按匹配分数排序
|
||||||
|
return recommendations.sort((a, b) => b.matchScore - a.matchScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算因子得分
|
||||||
|
*/
|
||||||
|
function calculateFactorScore(
|
||||||
|
value: number,
|
||||||
|
optimalRange: [number, number],
|
||||||
|
acceptableRange: [number, number]
|
||||||
|
): number {
|
||||||
|
// 最佳范围:100分
|
||||||
|
if (value >= optimalRange[0] && value <= optimalRange[1]) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可接受范围:60分
|
||||||
|
if (value >= acceptableRange[0] && value <= acceptableRange[1]) {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 偏离程度计算
|
||||||
|
const optimalMid = (optimalRange[0] + optimalRange[1]) / 2;
|
||||||
|
const deviation = Math.abs(value - optimalMid);
|
||||||
|
const maxDeviation = Math.max(
|
||||||
|
Math.abs(acceptableRange[0] - optimalMid),
|
||||||
|
Math.abs(acceptableRange[1] - optimalMid)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 根据偏离程度计算分数(0-60分)
|
||||||
|
const scoreRatio = Math.max(0, 1 - deviation / maxDeviation);
|
||||||
|
return Math.round(scoreRatio * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取得分状态
|
||||||
|
*/
|
||||||
|
function getScoreStatus(score: number): 'optimal' | 'acceptable' | 'poor' {
|
||||||
|
if (score >= 90) return 'optimal';
|
||||||
|
if (score >= 60) return 'acceptable';
|
||||||
|
return 'poor';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查风险条件
|
||||||
|
*/
|
||||||
|
function checkRiskCondition(condition: string, fieldFactors: FieldFactors): boolean {
|
||||||
|
// 简化的条件判断逻辑
|
||||||
|
if (condition.includes('温度低于') && fieldFactors.temperature !== undefined) {
|
||||||
|
const threshold = parseInt(condition.match(/\d+/)?.[0] || '0');
|
||||||
|
return fieldFactors.temperature < threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition.includes('温度超过') && fieldFactors.temperature !== undefined) {
|
||||||
|
const threshold = parseInt(condition.match(/\d+/)?.[0] || '0');
|
||||||
|
return fieldFactors.temperature > threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition.includes('降雨量低于') && fieldFactors.rainfall !== undefined) {
|
||||||
|
const threshold = parseInt(condition.match(/\d+/)?.[0] || '0');
|
||||||
|
return fieldFactors.rainfall < threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* 多因子评价服务类
|
||||||
|
* 提供地块适宜性评价、批量分析等功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EvaluationFactor,
|
||||||
|
SuitabilityResult,
|
||||||
|
FactorWeight,
|
||||||
|
BatchProgress,
|
||||||
|
DEFAULT_FACTORS,
|
||||||
|
WEIGHT_PRESETS,
|
||||||
|
MOCK_FIELDS
|
||||||
|
} from './multiFactorTypes';
|
||||||
|
|
||||||
|
export class MultiFactorService {
|
||||||
|
/**
|
||||||
|
* 计算单个因子的得分
|
||||||
|
*/
|
||||||
|
static calculateFactorScore(value: number, optimalRange: [number, number]): number {
|
||||||
|
const [min, max] = optimalRange;
|
||||||
|
const mid = (min + max) / 2;
|
||||||
|
const range = max - min;
|
||||||
|
|
||||||
|
// 如果在最佳范围内,得分较高
|
||||||
|
if (value >= min && value <= max) {
|
||||||
|
// 越接近中值得分越高 (85-100分)
|
||||||
|
const deviation = Math.abs(value - mid);
|
||||||
|
const deviationRatio = deviation / (range / 2);
|
||||||
|
return Math.max(85, 100 - deviationRatio * 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在范围外,根据偏离程度降低分数
|
||||||
|
const deviation = value < min ? min - value : value - max;
|
||||||
|
const deviationRatio = deviation / range;
|
||||||
|
|
||||||
|
// 偏离越大,分数越低 (最低20分)
|
||||||
|
return Math.max(20, 85 - deviationRatio * 65);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算综合评分(层次分析法AHP加权求和)
|
||||||
|
*/
|
||||||
|
static calculateTotalScore(factors: EvaluationFactor[]): number {
|
||||||
|
let totalScore = 0;
|
||||||
|
for (const factor of factors) {
|
||||||
|
totalScore += (factor.score * factor.weight) / 100;
|
||||||
|
}
|
||||||
|
return Math.round(totalScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据总分确定适宜性等级
|
||||||
|
*/
|
||||||
|
static getGrade(totalScore: number): '高度适宜' | '一般适宜' | '不适宜' {
|
||||||
|
if (totalScore >= 80) return '高度适宜';
|
||||||
|
if (totalScore >= 60) return '一般适宜';
|
||||||
|
return '不适宜';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从空间分析服务读取地块因子数据
|
||||||
|
*/
|
||||||
|
static fetchFieldFactorsFromSpatialService(fieldId: string, weights: FactorWeight[]): EvaluationFactor[] {
|
||||||
|
// 模拟读取地块的土壤因子数据
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'ph',
|
||||||
|
name: 'pH值',
|
||||||
|
value: 6.0 + Math.random() * 2, // 6.0-8.0
|
||||||
|
weight: weights.find(f => f.id === 'ph')?.weight || 20,
|
||||||
|
unit: '',
|
||||||
|
optimalRange: [6.5, 7.5],
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'organic',
|
||||||
|
name: '有机质含量',
|
||||||
|
value: 15 + Math.random() * 20, // 15-35 g/kg
|
||||||
|
weight: weights.find(f => f.id === 'organic')?.weight || 25,
|
||||||
|
unit: 'g/kg',
|
||||||
|
optimalRange: [20, 30],
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'depth',
|
||||||
|
name: '土层厚度',
|
||||||
|
value: 30 + Math.random() * 70, // 30-100 cm
|
||||||
|
weight: weights.find(f => f.id === 'depth')?.weight || 20,
|
||||||
|
unit: 'cm',
|
||||||
|
optimalRange: [50, 80],
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nitrogen',
|
||||||
|
name: '全氮',
|
||||||
|
value: 0.8 + Math.random() * 1.5, // 0.8-2.3 g/kg
|
||||||
|
weight: weights.find(f => f.id === 'nitrogen')?.weight || 10,
|
||||||
|
unit: 'g/kg',
|
||||||
|
optimalRange: [1.0, 2.0],
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'phosphorus',
|
||||||
|
name: '全磷',
|
||||||
|
value: 0.4 + Math.random() * 1.2, // 0.4-1.6 g/kg
|
||||||
|
weight: weights.find(f => f.id === 'phosphorus')?.weight || 10,
|
||||||
|
unit: 'g/kg',
|
||||||
|
optimalRange: [0.6, 1.2],
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'potassium',
|
||||||
|
name: '全钾',
|
||||||
|
value: 12 + Math.random() * 13, // 12-25 g/kg
|
||||||
|
weight: weights.find(f => f.id === 'potassium')?.weight || 10,
|
||||||
|
unit: 'g/kg',
|
||||||
|
optimalRange: [15, 20],
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'drainage',
|
||||||
|
name: '排水性',
|
||||||
|
value: 60 + Math.random() * 40, // 60-100分
|
||||||
|
weight: weights.find(f => f.id === 'drainage')?.weight || 5,
|
||||||
|
unit: '',
|
||||||
|
optimalRange: [70, 90],
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成地块适宜性评价结果
|
||||||
|
*/
|
||||||
|
static generateEvaluationResult(fieldId: string, weights: FactorWeight[]): SuitabilityResult {
|
||||||
|
const field = MOCK_FIELDS.find(f => f.id === fieldId);
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(`Field ${fieldId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从空间分析服务读取因子数据
|
||||||
|
const factors = MultiFactorService.fetchFieldFactorsFromSpatialService(fieldId, weights);
|
||||||
|
|
||||||
|
// 计算每个因子的得分
|
||||||
|
const scoredFactors = factors.map(factor => ({
|
||||||
|
...factor,
|
||||||
|
score: MultiFactorService.calculateFactorScore(factor.value, factor.optimalRange)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 计算综合得分(加权汇总)
|
||||||
|
const totalScore = MultiFactorService.calculateTotalScore(scoredFactors);
|
||||||
|
|
||||||
|
// 确定适宜性等级
|
||||||
|
const grade = MultiFactorService.getGrade(totalScore);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
fieldName: field.name,
|
||||||
|
totalScore,
|
||||||
|
grade,
|
||||||
|
factors: scoredFactors,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量分析所有地块
|
||||||
|
*/
|
||||||
|
static async runBatchAnalysis(
|
||||||
|
weights: FactorWeight[],
|
||||||
|
onProgress?: (progress: BatchProgress) => void
|
||||||
|
): Promise<SuitabilityResult[]> {
|
||||||
|
const totalFields = MOCK_FIELDS.length;
|
||||||
|
const results: SuitabilityResult[] = [];
|
||||||
|
let highSuitability = 0;
|
||||||
|
let mediumSuitability = 0;
|
||||||
|
let lowSuitability = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFields; i++) {
|
||||||
|
const field = MOCK_FIELDS[i];
|
||||||
|
|
||||||
|
// 模拟处理延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// 生成评价结果
|
||||||
|
const result = MultiFactorService.generateEvaluationResult(field.id, weights);
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
if (result.grade === '高度适宜') highSuitability++;
|
||||||
|
else if (result.grade === '一般适宜') mediumSuitability++;
|
||||||
|
else lowSuitability++;
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
total: totalFields,
|
||||||
|
processed: i + 1,
|
||||||
|
highSuitability,
|
||||||
|
mediumSuitability,
|
||||||
|
lowSuitability,
|
||||||
|
currentField: field.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证权重总和
|
||||||
|
*/
|
||||||
|
static validateWeights(weights: FactorWeight[]): { isValid: boolean; totalWeight: number } {
|
||||||
|
const totalWeight = weights.reduce((sum, f) => sum + f.weight, 0);
|
||||||
|
return {
|
||||||
|
isValid: totalWeight === 100,
|
||||||
|
totalWeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预设权重方案
|
||||||
|
*/
|
||||||
|
static getWeightPreset(preset: 'grain' | 'economic' | 'default'): FactorWeight[] {
|
||||||
|
switch (preset) {
|
||||||
|
case 'grain':
|
||||||
|
return [...WEIGHT_PRESETS.grain];
|
||||||
|
case 'economic':
|
||||||
|
return [...WEIGHT_PRESETS.economic];
|
||||||
|
default:
|
||||||
|
return [...DEFAULT_FACTORS];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新权重配置
|
||||||
|
*/
|
||||||
|
static updateWeight(weights: FactorWeight[], factorId: string, newWeight: number): FactorWeight[] {
|
||||||
|
return weights.map(f => f.id === factorId ? { ...f, weight: newWeight } : f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置权重到默认值
|
||||||
|
*/
|
||||||
|
static resetWeights(): FactorWeight[] {
|
||||||
|
return [...DEFAULT_FACTORS];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成模拟评价数据(用于演示)
|
||||||
|
*/
|
||||||
|
static generateMockEvaluationData(): SuitabilityResult[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldId: 'field-1',
|
||||||
|
fieldName: '东区1号地',
|
||||||
|
totalScore: 87,
|
||||||
|
grade: '高度适宜',
|
||||||
|
factors: [
|
||||||
|
{ id: 'ph', name: 'pH值', value: 6.5, weight: 20, unit: '', optimalRange: [6.0, 7.5], score: 95 },
|
||||||
|
{ id: 'organic', name: '有机质含量', value: 32, weight: 25, unit: 'g/kg', optimalRange: [25, 40], score: 90 },
|
||||||
|
{ id: 'depth', name: '土层厚度', value: 85, weight: 20, unit: 'cm', optimalRange: [60, 100], score: 88 },
|
||||||
|
{ id: 'nitrogen', name: '全氮', value: 1.8, weight: 10, unit: 'g/kg', optimalRange: [1.5, 2.5], score: 85 },
|
||||||
|
{ id: 'phosphorus', name: '全磷', value: 1.2, weight: 10, unit: 'g/kg', optimalRange: [1.0, 2.0], score: 80 },
|
||||||
|
{ id: 'potassium', name: '全钾', value: 18, weight: 10, unit: 'g/kg', optimalRange: [15, 25], score: 82 },
|
||||||
|
{ id: 'drainage', name: '排水性', value: 4, weight: 5, unit: '', optimalRange: [3, 5], score: 90 },
|
||||||
|
],
|
||||||
|
timestamp: '2024-10-15 14:30',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldId: 'field-2',
|
||||||
|
fieldName: '西区2号地',
|
||||||
|
totalScore: 72,
|
||||||
|
grade: '一般适宜',
|
||||||
|
factors: [
|
||||||
|
{ id: 'ph', name: 'pH值', value: 7.8, weight: 20, unit: '', optimalRange: [6.0, 7.5], score: 65 },
|
||||||
|
{ id: 'organic', name: '有机质含量', value: 22, weight: 25, unit: 'g/kg', optimalRange: [25, 40], score: 70 },
|
||||||
|
{ id: 'depth', name: '土层厚度', value: 55, weight: 20, unit: 'cm', optimalRange: [60, 100], score: 68 },
|
||||||
|
{ id: 'nitrogen', name: '全氮', value: 1.3, weight: 10, unit: 'g/kg', optimalRange: [1.5, 2.5], score: 72 },
|
||||||
|
{ id: 'phosphorus', name: '全磷', value: 0.9, weight: 10, unit: 'g/kg', optimalRange: [1.0, 2.0], score: 75 },
|
||||||
|
{ id: 'potassium', name: '全钾', value: 14, weight: 10, unit: 'g/kg', optimalRange: [15, 25], score: 78 },
|
||||||
|
{ id: 'drainage', name: '排水性', value: 3, weight: 5, unit: '', optimalRange: [3, 5], score: 85 },
|
||||||
|
],
|
||||||
|
timestamp: '2024-10-15 14:28',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldId: 'field-3',
|
||||||
|
fieldName: '南区3号地',
|
||||||
|
totalScore: 58,
|
||||||
|
grade: '不适宜',
|
||||||
|
factors: [
|
||||||
|
{ id: 'ph', name: 'pH值', value: 8.5, weight: 20, unit: '', optimalRange: [6.0, 7.5], score: 45 },
|
||||||
|
{ id: 'organic', name: '有机质含量', value: 15, weight: 25, unit: 'g/kg', optimalRange: [25, 40], score: 52 },
|
||||||
|
{ id: 'depth', name: '土层厚度', value: 42, weight: 20, unit: 'cm', optimalRange: [60, 100], score: 55 },
|
||||||
|
{ id: 'nitrogen', name: '全氮', value: 0.8, weight: 10, unit: 'g/kg', optimalRange: [1.5, 2.5], score: 60 },
|
||||||
|
{ id: 'phosphorus', name: '全磷', value: 0.6, weight: 10, unit: 'g/kg', optimalRange: [1.0, 2.0], score: 65 },
|
||||||
|
{ id: 'potassium', name: '全钾', value: 10, weight: 10, unit: 'g/kg', optimalRange: [15, 25], score: 58 },
|
||||||
|
{ id: 'drainage', name: '排水性', value: 2, weight: 5, unit: '', optimalRange: [3, 5], score: 70 },
|
||||||
|
],
|
||||||
|
timestamp: '2024-10-15 14:25',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成更多模拟地块数据(用于批量分析演示)
|
||||||
|
*/
|
||||||
|
static generateExtendedMockFields(): Array<{ id: string; name: string; code: string; area: number }> {
|
||||||
|
const extendedFields = [...MOCK_FIELDS];
|
||||||
|
|
||||||
|
// 生成更多模拟地块
|
||||||
|
for (let i = 4; i <= 68; i++) {
|
||||||
|
const zone = ['东区', '西区', '南区', '北区', '中心区'][Math.floor(Math.random() * 5)];
|
||||||
|
const number = Math.floor(Math.random() * 20) + 1;
|
||||||
|
const area = Math.round((Math.random() * 100 + 50) * 10) / 10;
|
||||||
|
|
||||||
|
extendedFields.push({
|
||||||
|
id: `field-${i}`,
|
||||||
|
name: `${zone}${number}号地`,
|
||||||
|
code: `DB${String(i).padStart(3, '0')}`,
|
||||||
|
area
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return extendedFields;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* 多因子评价类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface EvaluationFactor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
weight: number;
|
||||||
|
unit: string;
|
||||||
|
optimalRange: [number, number];
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuitabilityResult {
|
||||||
|
fieldId: string;
|
||||||
|
fieldName: string;
|
||||||
|
totalScore: number;
|
||||||
|
grade: '高度适宜' | '一般适宜' | '不适宜';
|
||||||
|
factors: EvaluationFactor[];
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactorWeight {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
weight: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchProgress {
|
||||||
|
total: number;
|
||||||
|
processed: number;
|
||||||
|
highSuitability: number;
|
||||||
|
mediumSuitability: number;
|
||||||
|
lowSuitability: number;
|
||||||
|
currentField: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 作物相关类型
|
||||||
|
export interface Crop {
|
||||||
|
id: string;
|
||||||
|
cropName: string;
|
||||||
|
category: '粮食作物' | '经济作物' | '蔬菜作物' | '果树作物';
|
||||||
|
description: string;
|
||||||
|
growthCycle: {
|
||||||
|
days: number;
|
||||||
|
seasons: string[];
|
||||||
|
};
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
organicMatter: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
soilDepth: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
nitrogen: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
phosphorus: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
potassium: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
drainage: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
};
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
rainfall: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
sunlight: { optimal: [number, number]; acceptable: [number, number] };
|
||||||
|
};
|
||||||
|
expectedYield: {
|
||||||
|
high: [number, number];
|
||||||
|
medium: [number, number];
|
||||||
|
low: [number, number];
|
||||||
|
};
|
||||||
|
riskFactors: RiskFactor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskFactor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
condition: string;
|
||||||
|
suggestion: string;
|
||||||
|
severity: 'high' | 'medium' | 'low';
|
||||||
|
category: 'soil' | 'climate' | 'nutrient' | 'disease' | 'pest';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CropRecommendation {
|
||||||
|
crop: Crop;
|
||||||
|
matchScore: number;
|
||||||
|
suitabilityLevel: '高度推荐' | '推荐' | '谨慎种植' | '不推荐';
|
||||||
|
matchDetails: MatchDetail[];
|
||||||
|
applicableRisks: RiskFactor[];
|
||||||
|
expectedYield: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchDetail {
|
||||||
|
factor: string;
|
||||||
|
value: number;
|
||||||
|
status: 'optimal' | 'acceptable' | 'poor';
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldFactors {
|
||||||
|
ph: number;
|
||||||
|
organic: number;
|
||||||
|
depth: number;
|
||||||
|
nitrogen: number;
|
||||||
|
phosphorus: number;
|
||||||
|
potassium: number;
|
||||||
|
drainage: number;
|
||||||
|
temperature?: number;
|
||||||
|
rainfall?: number;
|
||||||
|
sunlight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评价因子配置
|
||||||
|
export const DEFAULT_FACTORS: FactorWeight[] = [
|
||||||
|
{ id: 'ph', name: 'pH值', weight: 20, unit: '' },
|
||||||
|
{ id: 'organic', name: '有机质含量', weight: 25, unit: 'g/kg' },
|
||||||
|
{ id: 'depth', name: '土层厚度', weight: 20, unit: 'cm' },
|
||||||
|
{ id: 'nitrogen', name: '全氮', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'phosphorus', name: '全磷', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'potassium', name: '全钾', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'drainage', name: '排水性', weight: 5, unit: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 预设权重方案
|
||||||
|
export const WEIGHT_PRESETS = {
|
||||||
|
grain: [
|
||||||
|
{ id: 'ph', name: 'pH值', weight: 20, unit: '' },
|
||||||
|
{ id: 'organic', name: '有机质含量', weight: 30, unit: 'g/kg' },
|
||||||
|
{ id: 'depth', name: '土层厚度', weight: 25, unit: 'cm' },
|
||||||
|
{ id: 'nitrogen', name: '全氮', weight: 8, unit: 'g/kg' },
|
||||||
|
{ id: 'phosphorus', name: '全磷', weight: 8, unit: 'g/kg' },
|
||||||
|
{ id: 'potassium', name: '全钾', weight: 8, unit: 'g/kg' },
|
||||||
|
{ id: 'drainage', name: '排水性', weight: 1, unit: '' },
|
||||||
|
],
|
||||||
|
economic: [
|
||||||
|
{ id: 'ph', name: 'pH值', weight: 25, unit: '' },
|
||||||
|
{ id: 'organic', name: '有机质含量', weight: 35, unit: 'g/kg' },
|
||||||
|
{ id: 'depth', name: '土层厚度', weight: 15, unit: 'cm' },
|
||||||
|
{ id: 'nitrogen', name: '全氮', weight: 7, unit: 'g/kg' },
|
||||||
|
{ id: 'phosphorus', name: '全磷', weight: 7, unit: 'g/kg' },
|
||||||
|
{ id: 'potassium', name: '全钾', weight: 7, unit: 'g/kg' },
|
||||||
|
{ id: 'drainage', name: '排水性', weight: 4, unit: '' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 地块模拟数据
|
||||||
|
export const MOCK_FIELDS = [
|
||||||
|
{ id: 'field-1', name: '东区1号地', code: 'DB001', area: 85.5 },
|
||||||
|
{ id: 'field-2', name: '西区2号地', code: 'DB002', area: 92.3 },
|
||||||
|
{ id: 'field-3', name: '南区3号地', code: 'DB003', area: 78.7 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
export const getGradeColor = (grade: string): string => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return '#22c55e';
|
||||||
|
case '一般适宜': return '#eab308';
|
||||||
|
case '不适宜': return '#ef4444';
|
||||||
|
default: return '#6b7280';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getScoreColor = (score: number): string => {
|
||||||
|
if (score >= 80) return '#22c55e';
|
||||||
|
if (score >= 60) return '#eab308';
|
||||||
|
return '#ef4444';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSuitabilityLevelColor = (level: string): {
|
||||||
|
bg: string;
|
||||||
|
border: string;
|
||||||
|
text: string;
|
||||||
|
} => {
|
||||||
|
switch (level) {
|
||||||
|
case '高度推荐':
|
||||||
|
return { bg: '#22c55e', border: '#22c55e', text: '#22c55e' };
|
||||||
|
case '推荐':
|
||||||
|
return { bg: '#3b82f6', border: '#3b82f6', text: '#3b82f6' };
|
||||||
|
case '谨慎种植':
|
||||||
|
return { bg: '#eab308', border: '#eab308', text: '#eab308' };
|
||||||
|
default:
|
||||||
|
return { bg: '#6b7280', border: '#6b7280', text: '#6b7280' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (dateString: string): string => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { MultiFactorEvaluation } from './components/MultiFactorEvaluation';
|
||||||
|
|
||||||
|
export default function ComprehensivePage() {
|
||||||
|
return <MultiFactorEvaluation />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Leaf, AlertTriangle, ThermometerSun, Cloud, Sun } from 'lucide-react';
|
||||||
|
import { CropRecommendationState, SuitabilityResult } from './cropRecommendReducer';
|
||||||
|
|
||||||
|
// 模拟作物知识库数据
|
||||||
|
const cropKnowledgeBase = [
|
||||||
|
{
|
||||||
|
id: 'wheat',
|
||||||
|
cropName: '小麦',
|
||||||
|
category: '粮食作物',
|
||||||
|
description: '适应性强的主粮作物,对土壤要求较宽泛,耐寒性好,适合北方地区种植。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 220,
|
||||||
|
seasons: ['春季', '秋季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.5, 7.5], acceptable: [6.0, 8.0] },
|
||||||
|
organicMatter: { optimal: [25, 35], acceptable: [20, 40] },
|
||||||
|
soilDepth: { optimal: [60, 100], acceptable: [40, 120] },
|
||||||
|
nitrogen: { optimal: [1.5, 2.5], acceptable: [1.0, 3.0] },
|
||||||
|
phosphorus: { optimal: [1.0, 2.0], acceptable: [0.6, 2.5] },
|
||||||
|
potassium: { optimal: [15, 25], acceptable: [10, 30] },
|
||||||
|
drainage: { optimal: [3, 5], acceptable: [2, 5] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [15, 22], acceptable: [10, 25] },
|
||||||
|
rainfall: { optimal: [400, 600], acceptable: [300, 800] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [400, 500],
|
||||||
|
medium: [300, 400],
|
||||||
|
low: [200, 300]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'wheat-rust',
|
||||||
|
name: '锈病风险',
|
||||||
|
condition: '湿度过高、温度适宜',
|
||||||
|
severity: 'medium' as const,
|
||||||
|
suggestion: '选择抗病品种,合理密植,及时防治'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wheat-drought',
|
||||||
|
name: '干旱风险',
|
||||||
|
condition: '降雨量不足400mm',
|
||||||
|
severity: 'high' as const,
|
||||||
|
suggestion: '加强灌溉设施建设,选择抗旱品种'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'corn',
|
||||||
|
cropName: '玉米',
|
||||||
|
category: '粮食作物',
|
||||||
|
description: '高产作物,对温度要求较高,需水量大,适合水热条件良好的地区。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 120,
|
||||||
|
seasons: ['春季', '夏季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [30, 40], acceptable: [25, 45] },
|
||||||
|
soilDepth: { optimal: [80, 120], acceptable: [60, 150] },
|
||||||
|
nitrogen: { optimal: [2.0, 3.0], acceptable: [1.5, 3.5] },
|
||||||
|
phosphorus: { optimal: [1.2, 2.5], acceptable: [0.8, 3.0] },
|
||||||
|
potassium: { optimal: [20, 30], acceptable: [15, 35] },
|
||||||
|
drainage: { optimal: [3, 5], acceptable: [2, 5] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [20, 28], acceptable: [15, 32] },
|
||||||
|
rainfall: { optimal: [500, 800], acceptable: [400, 1000] },
|
||||||
|
sunlight: { optimal: [7, 9], acceptable: [6, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [600, 800],
|
||||||
|
medium: [400, 600],
|
||||||
|
low: [250, 400]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'corn-borer',
|
||||||
|
name: '玉米螟',
|
||||||
|
condition: '温度适宜、湿度适中',
|
||||||
|
severity: 'medium' as const,
|
||||||
|
suggestion: '生物防治与化学防治结合,适时播种'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'corn-drought',
|
||||||
|
name: '花期干旱',
|
||||||
|
condition: '开花期降雨不足',
|
||||||
|
severity: 'high' as const,
|
||||||
|
suggestion: '保证花期灌溉,选择耐旱品种'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soybean',
|
||||||
|
cropName: '大豆',
|
||||||
|
category: '经济作物',
|
||||||
|
description: '豆科作物,具有固氮能力,对土壤肥力要求较低,适合轮作种植。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 100,
|
||||||
|
seasons: ['春季', '夏季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [25, 35], acceptable: [20, 40] },
|
||||||
|
soilDepth: { optimal: [50, 80], acceptable: [40, 100] },
|
||||||
|
nitrogen: { optimal: [1.0, 2.0], acceptable: [0.5, 2.5] },
|
||||||
|
phosphorus: { optimal: [0.8, 1.8], acceptable: [0.5, 2.5] },
|
||||||
|
potassium: { optimal: [15, 25], acceptable: [10, 30] },
|
||||||
|
drainage: { optimal: [3, 5], acceptable: [2, 5] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [18, 25], acceptable: [15, 28] },
|
||||||
|
rainfall: { optimal: [450, 700], acceptable: [350, 900] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 9] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [250, 350],
|
||||||
|
medium: [180, 250],
|
||||||
|
low: [120, 180]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'soybean-disease',
|
||||||
|
name: '病害风险',
|
||||||
|
condition: '高温高湿环境',
|
||||||
|
severity: 'medium' as const,
|
||||||
|
suggestion: '选择抗病品种,合理轮作,加强田间管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
interface CropRecommendationsProps {
|
||||||
|
state: CropRecommendationState;
|
||||||
|
currentResult: SuitabilityResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CropRecommendations({ state, currentResult }: CropRecommendationsProps) {
|
||||||
|
// 匹配作物推荐
|
||||||
|
const matchCropsForField = (fieldFactors: any) => {
|
||||||
|
return cropKnowledgeBase.map(crop => {
|
||||||
|
let totalScore = 0;
|
||||||
|
let factorCount = 0;
|
||||||
|
const matchDetails: any[] = [];
|
||||||
|
|
||||||
|
// 评估土壤因子匹配度
|
||||||
|
Object.entries(crop.soilRequirements).forEach(([factor, requirements]: [string, any]) => {
|
||||||
|
if (fieldFactors[factor]) {
|
||||||
|
const value = fieldFactors[factor];
|
||||||
|
const { optimal, acceptable } = requirements;
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
let status = '偏离';
|
||||||
|
|
||||||
|
if (value >= optimal[0] && value <= optimal[1]) {
|
||||||
|
score = 100;
|
||||||
|
status = '最佳';
|
||||||
|
} else if (value >= acceptable[0] && value <= acceptable[1]) {
|
||||||
|
const deviation = Math.min(
|
||||||
|
Math.abs(value - optimal[0]),
|
||||||
|
Math.abs(value - optimal[1])
|
||||||
|
);
|
||||||
|
const range = optimal[1] - optimal[0];
|
||||||
|
score = Math.max(60, 100 - (deviation / range) * 40);
|
||||||
|
status = '可接受';
|
||||||
|
} else {
|
||||||
|
score = Math.max(0, 60 - Math.min(
|
||||||
|
Math.abs(value - acceptable[0]),
|
||||||
|
Math.abs(value - acceptable[1])
|
||||||
|
) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalScore += score;
|
||||||
|
factorCount++;
|
||||||
|
|
||||||
|
matchDetails.push({
|
||||||
|
factor: factor === 'ph' ? 'pH值' :
|
||||||
|
factor === 'organicMatter' ? '有机质' :
|
||||||
|
factor === 'soilDepth' ? '土层厚度' :
|
||||||
|
factor === 'nitrogen' ? '全氮' :
|
||||||
|
factor === 'phosphorus' ? '全磷' :
|
||||||
|
factor === 'potassium' ? '全钾' : '排水性',
|
||||||
|
value,
|
||||||
|
score,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 评估气候因子(简化处理)
|
||||||
|
if (fieldFactors.temperature) {
|
||||||
|
const tempScore = fieldFactors.temperature >= 18 && fieldFactors.temperature <= 25 ? 90 : 70;
|
||||||
|
totalScore += tempScore;
|
||||||
|
factorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldFactors.rainfall) {
|
||||||
|
const rainScore = fieldFactors.rainfall >= 500 && fieldFactors.rainfall <= 800 ? 90 : 70;
|
||||||
|
totalScore += rainScore;
|
||||||
|
factorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchScore = Math.round(totalScore / factorCount);
|
||||||
|
|
||||||
|
// 确定适宜性等级
|
||||||
|
let suitabilityLevel = '不推荐';
|
||||||
|
if (matchScore >= 85) suitabilityLevel = '高度推荐';
|
||||||
|
else if (matchScore >= 70) suitabilityLevel = '推荐';
|
||||||
|
else if (matchScore >= 50) suitabilityLevel = '谨慎种植';
|
||||||
|
|
||||||
|
// 根据适宜性等级选择产量区间
|
||||||
|
let expectedYield = crop.expectedYield.low;
|
||||||
|
if (suitabilityLevel === '高度推荐') expectedYield = crop.expectedYield.high;
|
||||||
|
else if (suitabilityLevel === '推荐') expectedYield = crop.expectedYield.medium;
|
||||||
|
|
||||||
|
// 识别适用风险
|
||||||
|
const applicableRisks = crop.riskFactors.filter(risk => {
|
||||||
|
if (risk.id.includes('drought') && fieldFactors.rainfall < 400) return true;
|
||||||
|
if (risk.id.includes('rust') && fieldFactors.temperature >= 15 && fieldFactors.temperature <= 22) return true;
|
||||||
|
return true; // 简化处理,默认显示所有风险
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
crop,
|
||||||
|
matchScore,
|
||||||
|
suitabilityLevel,
|
||||||
|
matchDetails,
|
||||||
|
applicableRisks,
|
||||||
|
expectedYield
|
||||||
|
};
|
||||||
|
}).sort((a, b) => b.matchScore - a.matchScore);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取地块因子数据
|
||||||
|
const fieldFactors = {
|
||||||
|
ph: currentResult.factors.find(f => f.id === 'ph')?.value || 0,
|
||||||
|
organic: currentResult.factors.find(f => f.id === 'organic')?.value || 0,
|
||||||
|
depth: currentResult.factors.find(f => f.id === 'depth')?.value || 0,
|
||||||
|
nitrogen: currentResult.factors.find(f => f.id === 'nitrogen')?.value || 0,
|
||||||
|
phosphorus: currentResult.factors.find(f => f.id === 'phosphorus')?.value || 0,
|
||||||
|
potassium: currentResult.factors.find(f => f.id === 'potassium')?.value || 0,
|
||||||
|
drainage: currentResult.factors.find(f => f.id === 'drainage')?.value || 0,
|
||||||
|
temperature: 22, // 模拟年均温度
|
||||||
|
rainfall: 800, // 模拟年均降雨量
|
||||||
|
};
|
||||||
|
|
||||||
|
// 匹配推荐作物
|
||||||
|
const recommendations = matchCropsForField(fieldFactors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="flex items-center gap-2">
|
||||||
|
<Leaf className="w-5 h-5 text-green-600" />
|
||||||
|
智能作物推荐清单
|
||||||
|
</h3>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
基于{cropKnowledgeBase.length}种作物知识库匹配
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{recommendations.map((recommendation, index) => {
|
||||||
|
const { crop, matchScore, suitabilityLevel, matchDetails, applicableRisks, expectedYield } = recommendation;
|
||||||
|
|
||||||
|
// 根据适宜性等级设置颜色
|
||||||
|
const levelColor =
|
||||||
|
suitabilityLevel === '高度推荐' ? { bg: 'bg-green-500', border: '#22c55e', text: 'text-green-600 dark:text-green-400' } :
|
||||||
|
suitabilityLevel === '推荐' ? { bg: 'bg-blue-500', border: '#3b82f6', text: 'text-blue-600 dark:text-blue-400' } :
|
||||||
|
suitabilityLevel === '谨慎种植' ? { bg: 'bg-yellow-500', border: '#eab308', text: 'text-yellow-600 dark:text-yellow-400' } :
|
||||||
|
{ bg: 'bg-gray-500', border: '#6b7280', text: 'text-gray-600 dark:text-gray-400' };
|
||||||
|
|
||||||
|
// 只显示高度推荐和推荐的作物
|
||||||
|
if (suitabilityLevel === '不推荐') return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={index} className="p-5 border-l-4 hover:shadow-lg transition-shadow" style={{ borderLeftColor: levelColor.border }}>
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900`}>
|
||||||
|
<Leaf className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="mb-1">{crop.cropName}</h4>
|
||||||
|
<Badge variant="outline" className="text-xs">{crop.category}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge className={`${levelColor.bg} text-white`}>
|
||||||
|
{suitabilityLevel}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">匹配度: {matchScore}分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">预期产量区间</p>
|
||||||
|
<p className={`text-lg font-medium ${levelColor.text}`}>
|
||||||
|
{expectedYield[0]}-{expectedYield[1]} kg/亩
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">生长周期: {crop.growthCycle.days}天</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 作物描述 */}
|
||||||
|
<p className="text-sm text-muted-foreground mb-3 p-2 bg-gray-50 dark:bg-gray-900 rounded">
|
||||||
|
{crop.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 土壤因子匹配详情 */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">土壤因子匹配情况:</p>
|
||||||
|
<div className="grid grid-cols-7 gap-2">
|
||||||
|
{matchDetails.map((detail, i) => (
|
||||||
|
<div key={i} className="p-2 bg-gray-50 dark:bg-gray-900 rounded text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">{detail.factor}</p>
|
||||||
|
<p className="text-xs font-medium mb-1">{detail.value.toFixed(1)}</p>
|
||||||
|
{detail.status === '最佳' ? (
|
||||||
|
<Badge className="bg-green-500 text-white" style={{ fontSize: '9px', padding: '1px 4px' }}>
|
||||||
|
最佳
|
||||||
|
</Badge>
|
||||||
|
) : detail.status === '可接受' ? (
|
||||||
|
<Badge className="bg-blue-500 text-white" style={{ fontSize: '9px', padding: '1px 4px' }}>
|
||||||
|
可接受
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="text-red-500" style={{ fontSize: '9px', padding: '1px 4px' }}>
|
||||||
|
偏离
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 气候要求 */}
|
||||||
|
<div className="grid grid-cols-3 gap-3 mb-3">
|
||||||
|
<div className="p-2 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<p className="text-xs text-blue-600 dark:text-blue-400 mb-1 flex items-center gap-1">
|
||||||
|
<ThermometerSun className="w-3 h-3" />
|
||||||
|
温度要求
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-blue-900 dark:text-blue-100">
|
||||||
|
{crop.climateRequirements.temperature.optimal[0]}-{crop.climateRequirements.temperature.optimal[1]}°C
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-cyan-50 dark:bg-cyan-950 rounded-lg">
|
||||||
|
<p className="text-xs text-cyan-600 dark:text-cyan-400 mb-1 flex items-center gap-1">
|
||||||
|
<Cloud className="w-3 h-3" />
|
||||||
|
降雨要求
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-cyan-900 dark:text-cyan-100">
|
||||||
|
{crop.climateRequirements.rainfall.optimal[0]}-{crop.climateRequirements.rainfall.optimal[1]}mm/年
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-amber-50 dark:bg-amber-950 rounded-lg">
|
||||||
|
<p className="text-xs text-amber-600 dark:text-amber-400 mb-1 flex items-center gap-1">
|
||||||
|
<Sun className="w-3 h-3" />
|
||||||
|
光照要求
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-amber-900 dark:text-amber-100">
|
||||||
|
{crop.climateRequirements.sunlight.optimal[0]}-{crop.climateRequirements.sunlight.optimal[1]}小时/天
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 风险提示 */}
|
||||||
|
{applicableRisks.length > 0 && (
|
||||||
|
<div className={`p-3 rounded-lg border ${
|
||||||
|
applicableRisks.some(r => r.severity === 'high')
|
||||||
|
? 'bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800'
|
||||||
|
: 'bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800'
|
||||||
|
}`}>
|
||||||
|
<p className="text-xs font-medium mb-2 flex items-center gap-1">
|
||||||
|
<AlertTriangle className={`w-4 h-4 ${
|
||||||
|
applicableRisks.some(r => r.severity === 'high')
|
||||||
|
? 'text-red-600 dark:text-red-400'
|
||||||
|
: 'text-orange-600 dark:text-orange-400'
|
||||||
|
}`} />
|
||||||
|
<span className={applicableRisks.some(r => r.severity === 'high') ? 'text-red-900 dark:text-red-100' : 'text-orange-900 dark:text-orange-100'}>
|
||||||
|
风险提示与应对建议
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{applicableRisks.map((risk, i) => (
|
||||||
|
<div key={i} className="text-xs">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Badge
|
||||||
|
className={
|
||||||
|
risk.severity === 'high' ? 'bg-red-500 text-white' :
|
||||||
|
risk.severity === 'medium' ? 'bg-orange-500 text-white' :
|
||||||
|
'bg-yellow-500 text-white'
|
||||||
|
}
|
||||||
|
style={{ fontSize: '9px', padding: '2px 6px', marginTop: '2px' }}
|
||||||
|
>
|
||||||
|
{risk.severity === 'high' ? '高风险' : risk.severity === 'medium' ? '中风险' : '低风险'}
|
||||||
|
</Badge>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`font-medium mb-0.5 ${
|
||||||
|
applicableRisks.some(r => r.severity === 'high')
|
||||||
|
? 'text-red-800 dark:text-red-200'
|
||||||
|
: 'text-orange-800 dark:text-orange-200'
|
||||||
|
}`}>
|
||||||
|
{risk.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mb-1">触发条件: {risk.condition}</p>
|
||||||
|
<p className={applicableRisks.some(r => r.severity === 'high') ? 'text-red-700 dark:text-red-300' : 'text-orange-700 dark:text-orange-300'}>
|
||||||
|
💡 {risk.suggestion}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 适宜季节 */}
|
||||||
|
<div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>适宜种植季节:</span>
|
||||||
|
{crop.growthCycle.seasons.map((season, i) => (
|
||||||
|
<Badge key={i} variant="outline" className="text-xs">
|
||||||
|
{season}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ThermometerSun, Leaf, Droplet } from 'lucide-react';
|
||||||
|
import { SuitabilityResult } from './cropRecommendReducer';
|
||||||
|
|
||||||
|
interface FieldEnvironmentOverviewProps {
|
||||||
|
currentResult: SuitabilityResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldEnvironmentOverview({ currentResult }: FieldEnvironmentOverviewProps) {
|
||||||
|
const getScoreColor = (score: number) => {
|
||||||
|
if (score >= 80) return 'text-green-600 dark:text-green-400';
|
||||||
|
if (score >= 60) return 'text-yellow-600 dark:text-yellow-400';
|
||||||
|
return 'text-red-600 dark:text-red-400';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return 'bg-green-500';
|
||||||
|
case '一般适宜': return 'bg-yellow-500';
|
||||||
|
case '不适宜': return 'bg-red-500';
|
||||||
|
default: return 'bg-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4 flex items-center gap-2">
|
||||||
|
<Leaf className="w-5 h-5 text-green-600" />
|
||||||
|
{currentResult.fieldName} - 环境参数概览
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4 mb-4">
|
||||||
|
<div className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-950 dark:to-blue-900 rounded-lg">
|
||||||
|
<p className="text-xs text-blue-600 dark:text-blue-400 mb-1 flex items-center gap-1">
|
||||||
|
<ThermometerSun className="w-3 h-3" />
|
||||||
|
pH值
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-medium text-blue-900 dark:text-blue-100">
|
||||||
|
{currentResult.factors.find(f => f.id === 'ph')?.value.toFixed(1)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||||
|
最佳: {currentResult.factors.find(f => f.id === 'ph')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'ph')?.optimalRange[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900 rounded-lg">
|
||||||
|
<p className="text-xs text-green-600 dark:text-green-400 mb-1 flex items-center gap-1">
|
||||||
|
<Leaf className="w-3 h-3" />
|
||||||
|
有机质
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-medium text-green-900 dark:text-green-100">
|
||||||
|
{currentResult.factors.find(f => f.id === 'organic')?.value.toFixed(1)} g/kg
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-green-700 dark:text-green-300 mt-1">
|
||||||
|
最佳: {currentResult.factors.find(f => f.id === 'organic')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'organic')?.optimalRange[1]} g/kg
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-amber-50 to-amber-100 dark:from-amber-950 dark:to-amber-900 rounded-lg">
|
||||||
|
<p className="text-xs text-amber-600 dark:text-amber-400 mb-1">土层厚度</p>
|
||||||
|
<p className="text-xl font-medium text-amber-900 dark:text-amber-100">
|
||||||
|
{currentResult.factors.find(f => f.id === 'depth')?.value.toFixed(0)} cm
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
|
||||||
|
最佳: {currentResult.factors.find(f => f.id === 'depth')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'depth')?.optimalRange[1]} cm
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-950 dark:to-purple-900 rounded-lg">
|
||||||
|
<p className="text-xs text-purple-600 dark:text-purple-400 mb-1 flex items-center gap-1">
|
||||||
|
<Droplet className="w-3 h-3" />
|
||||||
|
排水性
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-medium text-purple-900 dark:text-purple-100">
|
||||||
|
{currentResult.factors.find(f => f.id === 'drainage')?.value.toFixed(0)}分
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-purple-700 dark:text-purple-300 mt-1">
|
||||||
|
最佳: {currentResult.factors.find(f => f.id === 'drainage')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'drainage')?.optimalRange[1]} 分
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">全氮</p>
|
||||||
|
<p className="font-medium">{currentResult.factors.find(f => f.id === 'nitrogen')?.value.toFixed(2)} g/kg</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
最佳: {currentResult.factors.find(f => f.id === 'nitrogen')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'nitrogen')?.optimalRange[1]} g/kg
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">全磷</p>
|
||||||
|
<p className="font-medium">{currentResult.factors.find(f => f.id === 'phosphorus')?.value.toFixed(2)} g/kg</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
最佳: {currentResult.factors.find(f => f.id === 'phosphorus')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'phosphorus')?.optimalRange[1]} g/kg
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">全钾</p>
|
||||||
|
<p className="font-medium">{currentResult.factors.find(f => f.id === 'potassium')?.value.toFixed(1)} g/kg</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
最佳: {currentResult.factors.find(f => f.id === 'potassium')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'potassium')?.optimalRange[1]} g/kg
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-950 dark:to-blue-950 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<h4 className="text-green-900 dark:text-green-100 mb-3 font-medium">地块适宜性分析</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-green-800 dark:text-green-200 mb-2">
|
||||||
|
<span className="font-medium">综合评分:</span>
|
||||||
|
<span className={`ml-2 text-lg font-bold ${getScoreColor(currentResult.totalScore)}`}>
|
||||||
|
{currentResult.totalScore}分
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<Badge className={`${getGradeColor(currentResult.grade)} text-white`}>
|
||||||
|
{currentResult.grade}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-green-800 dark:text-green-200 mb-1">
|
||||||
|
<span className="font-medium">优秀因子:</span>
|
||||||
|
<span className="text-green-600 dark:text-green-400 font-medium ml-1">
|
||||||
|
{currentResult.factors.filter(f => f.score >= 80).length}项
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-green-800 dark:text-green-200">
|
||||||
|
<span className="font-medium">待改善因子:</span>
|
||||||
|
<span className="text-red-600 dark:text-red-400 font-medium ml-1">
|
||||||
|
{currentResult.factors.filter(f => f.score < 70).length}项
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-green-800 dark:text-green-200 mb-1">
|
||||||
|
<span className="font-medium">评价时间:</span>
|
||||||
|
<span className="text-muted-foreground ml-1">{currentResult.timestamp}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-green-800 dark:text-green-200">
|
||||||
|
<span className="font-medium">地块ID:</span>
|
||||||
|
<span className="text-muted-foreground ml-1">{currentResult.fieldId}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { BookOpen, Database, Leaf, Target, Droplet, Cloud, Sun, ThermometerSun, TrendingUp, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
// 作物知识库数据(与CropRecommendations.tsx保持一致)
|
||||||
|
const cropKnowledgeBase = [
|
||||||
|
{
|
||||||
|
id: 'wheat',
|
||||||
|
cropName: '小麦',
|
||||||
|
category: '粮食作物',
|
||||||
|
description: '适应性强的主粮作物,对土壤要求较宽泛,耐寒性好,适合北方地区种植。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 220,
|
||||||
|
seasons: ['春季', '秋季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.5, 7.5], acceptable: [6.0, 8.0] },
|
||||||
|
organicMatter: { optimal: [25, 35], acceptable: [20, 40] },
|
||||||
|
soilDepth: { optimal: [60, 100], acceptable: [40, 120] },
|
||||||
|
nitrogen: { optimal: [1.5, 2.5], acceptable: [1.0, 3.0] },
|
||||||
|
phosphorus: { optimal: [1.0, 2.0], acceptable: [0.6, 2.5] },
|
||||||
|
potassium: { optimal: [15, 25], acceptable: [10, 30] },
|
||||||
|
drainage: { optimal: [3, 5], acceptable: [2, 5] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [15, 22], acceptable: [10, 25] },
|
||||||
|
rainfall: { optimal: [400, 600], acceptable: [300, 800] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [400, 500],
|
||||||
|
medium: [300, 400],
|
||||||
|
low: [200, 300]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'wheat-rust',
|
||||||
|
name: '锈病风险',
|
||||||
|
condition: '湿度过高、温度适宜',
|
||||||
|
severity: 'medium' as const,
|
||||||
|
suggestion: '选择抗病品种,合理密植,及时防治'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wheat-drought',
|
||||||
|
name: '干旱风险',
|
||||||
|
condition: '降雨量不足400mm',
|
||||||
|
severity: 'high' as const,
|
||||||
|
suggestion: '加强灌溉设施建设,选择抗旱品种'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'corn',
|
||||||
|
cropName: '玉米',
|
||||||
|
category: '粮食作物',
|
||||||
|
description: '高产作物,对温度要求较高,需水量大,适合水热条件良好的地区。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 120,
|
||||||
|
seasons: ['春季', '夏季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [30, 40], acceptable: [25, 45] },
|
||||||
|
soilDepth: { optimal: [80, 120], acceptable: [60, 150] },
|
||||||
|
nitrogen: { optimal: [2.0, 3.0], acceptable: [1.5, 3.5] },
|
||||||
|
phosphorus: { optimal: [1.2, 2.5], acceptable: [0.8, 3.0] },
|
||||||
|
potassium: { optimal: [20, 30], acceptable: [15, 35] },
|
||||||
|
drainage: { optimal: [3, 5], acceptable: [2, 5] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [20, 28], acceptable: [15, 32] },
|
||||||
|
rainfall: { optimal: [500, 800], acceptable: [400, 1000] },
|
||||||
|
sunlight: { optimal: [7, 9], acceptable: [6, 10] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [600, 800],
|
||||||
|
medium: [400, 600],
|
||||||
|
low: [250, 400]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'corn-borer',
|
||||||
|
name: '玉米螟',
|
||||||
|
condition: '温度适宜、湿度适中',
|
||||||
|
severity: 'medium' as const,
|
||||||
|
suggestion: '生物防治与化学防治结合,适时播种'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'corn-drought',
|
||||||
|
name: '花期干旱',
|
||||||
|
condition: '开花期降雨不足',
|
||||||
|
severity: 'high' as const,
|
||||||
|
suggestion: '保证花期灌溉,选择耐旱品种'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soybean',
|
||||||
|
cropName: '大豆',
|
||||||
|
category: '经济作物',
|
||||||
|
description: '豆科作物,具有固氮能力,对土壤肥力要求较低,适合轮作种植。',
|
||||||
|
growthCycle: {
|
||||||
|
days: 100,
|
||||||
|
seasons: ['春季', '夏季']
|
||||||
|
},
|
||||||
|
soilRequirements: {
|
||||||
|
ph: { optimal: [6.0, 7.0], acceptable: [5.5, 7.5] },
|
||||||
|
organicMatter: { optimal: [25, 35], acceptable: [20, 40] },
|
||||||
|
soilDepth: { optimal: [50, 80], acceptable: [40, 100] },
|
||||||
|
nitrogen: { optimal: [1.0, 2.0], acceptable: [0.5, 2.5] },
|
||||||
|
phosphorus: { optimal: [0.8, 1.8], acceptable: [0.5, 2.5] },
|
||||||
|
potassium: { optimal: [15, 25], acceptable: [10, 30] },
|
||||||
|
drainage: { optimal: [3, 5], acceptable: [2, 5] }
|
||||||
|
},
|
||||||
|
climateRequirements: {
|
||||||
|
temperature: { optimal: [18, 25], acceptable: [15, 28] },
|
||||||
|
rainfall: { optimal: [450, 700], acceptable: [350, 900] },
|
||||||
|
sunlight: { optimal: [6, 8], acceptable: [5, 9] }
|
||||||
|
},
|
||||||
|
expectedYield: {
|
||||||
|
high: [250, 350],
|
||||||
|
medium: [180, 250],
|
||||||
|
low: [120, 180]
|
||||||
|
},
|
||||||
|
riskFactors: [
|
||||||
|
{
|
||||||
|
id: 'soybean-disease',
|
||||||
|
name: '病害风险',
|
||||||
|
condition: '高温高湿环境',
|
||||||
|
severity: 'medium' as const,
|
||||||
|
suggestion: '选择抗病品种,合理轮作,加强田间管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
interface KnowledgeBaseDialogProps {
|
||||||
|
showKnowledgeBase: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KnowledgeBaseDialog({ showKnowledgeBase, onClose }: KnowledgeBaseDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={showKnowledgeBase} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<BookOpen className="w-5 h-5 text-green-600" />
|
||||||
|
作物-环境知识库
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
查看各种作物的土壤环境要求、气候要求、预期产量及风险因子
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 mt-4">
|
||||||
|
{/* 知识库统计 */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<Card className="p-4 bg-green-50 dark:bg-green-950">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-green-700 dark:text-green-300 mb-1">作物总数</p>
|
||||||
|
<p className="text-2xl font-semibold text-green-900 dark:text-green-100">{cropKnowledgeBase.length}</p>
|
||||||
|
</div>
|
||||||
|
<Database className="w-8 h-8 text-green-600 dark:text-green-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-blue-700 dark:text-blue-300 mb-1">粮食作物</p>
|
||||||
|
<p className="text-2xl font-semibold text-blue-900 dark:text-blue-100">
|
||||||
|
{cropKnowledgeBase.filter(c => c.category === '粮食作物').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Leaf className="w-8 h-8 text-blue-600 dark:text-blue-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-amber-50 dark:bg-amber-950">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-amber-700 dark:text-amber-300 mb-1">经济作物</p>
|
||||||
|
<p className="text-2xl font-semibold text-amber-900 dark:text-amber-100">
|
||||||
|
{cropKnowledgeBase.filter(c => c.category === '经济作物').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Target className="w-8 h-8 text-amber-600 dark:text-amber-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-purple-50 dark:bg-purple-950">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-purple-700 dark:text-purple-300 mb-1">蔬菜作物</p>
|
||||||
|
<p className="text-2xl font-semibold text-purple-900 dark:text-purple-100">
|
||||||
|
{cropKnowledgeBase.filter(c => c.category === '蔬菜作物').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Droplet className="w-8 h-8 text-purple-600 dark:text-purple-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 作物列表 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{cropKnowledgeBase.map((crop) => (
|
||||||
|
<Card key={crop.id} className="p-6">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h3 className="text-green-800 dark:text-green-200">{crop.cropName}</h3>
|
||||||
|
<Badge variant={
|
||||||
|
crop.category === '粮食作物' ? 'default' :
|
||||||
|
crop.category === '经济作物' ? 'secondary' :
|
||||||
|
'outline'
|
||||||
|
}>
|
||||||
|
{crop.category}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">{crop.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">生长周期</p>
|
||||||
|
<p className="text-lg font-semibold text-green-700 dark:text-green-300">{crop.growthCycle.days}天</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{crop.growthCycle.seasons.join('、')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{/* 土壤环境要求 */}
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<Database className="w-4 h-4 text-green-600" />
|
||||||
|
土壤环境要求
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground">pH值:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
最佳 {crop.soilRequirements.ph.optimal[0]}-{crop.soilRequirements.ph.optimal[1]}
|
||||||
|
<span className="text-muted-foreground text-xs ml-2">
|
||||||
|
(可接受 {crop.soilRequirements.ph.acceptable[0]}-{crop.soilRequirements.ph.acceptable[1]})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground">有机质:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.soilRequirements.organicMatter.optimal[0]}-{crop.soilRequirements.organicMatter.optimal[1]} g/kg
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground">土层厚度:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.soilRequirements.soilDepth.optimal[0]}-{crop.soilRequirements.soilDepth.optimal[1]} cm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground">全氮:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.soilRequirements.nitrogen.optimal[0]}-{crop.soilRequirements.nitrogen.optimal[1]} g/kg
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground">全磷:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.soilRequirements.phosphorus.optimal[0]}-{crop.soilRequirements.phosphorus.optimal[1]} g/kg
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground">全钾:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.soilRequirements.potassium.optimal[0]}-{crop.soilRequirements.potassium.optimal[1]} g/kg
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5">
|
||||||
|
<span className="text-muted-foreground">排水性评分:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.soilRequirements.drainage.optimal[0]}-{crop.soilRequirements.drainage.optimal[1]} 分
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 气候要求与产量 */}
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<Cloud className="w-4 h-4 text-blue-600" />
|
||||||
|
气候要求
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 text-sm mb-4">
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground flex items-center gap-1">
|
||||||
|
<ThermometerSun className="w-3 h-3" />
|
||||||
|
温度:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.climateRequirements.temperature.optimal[0]}-{crop.climateRequirements.temperature.optimal[1]} °C
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 border-b">
|
||||||
|
<span className="text-muted-foreground flex items-center gap-1">
|
||||||
|
<Droplet className="w-3 h-3" />
|
||||||
|
降雨量:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.climateRequirements.rainfall.optimal[0]}-{crop.climateRequirements.rainfall.optimal[1]} mm/年
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5">
|
||||||
|
<span className="text-muted-foreground flex items-center gap-1">
|
||||||
|
<Sun className="w-3 h-3" />
|
||||||
|
光照:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{crop.climateRequirements.sunlight.optimal[0]}-{crop.climateRequirements.sunlight.optimal[1]} 小时/天
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="mb-3 flex items-center gap-2 mt-4">
|
||||||
|
<TrendingUp className="w-4 h-4 text-amber-600" />
|
||||||
|
预期产量 (kg/亩)
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between py-1.5 border-b bg-green-50 dark:bg-green-950 px-2 rounded">
|
||||||
|
<span className="text-green-700 dark:text-green-300">高适宜性:</span>
|
||||||
|
<span className="font-semibold text-green-800 dark:text-green-200">
|
||||||
|
{crop.expectedYield.high[0]}-{crop.expectedYield.high[1]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 border-b bg-blue-50 dark:bg-blue-950 px-2 rounded">
|
||||||
|
<span className="text-blue-700 dark:text-blue-300">中等适宜性:</span>
|
||||||
|
<span className="font-semibold text-blue-800 dark:text-blue-200">
|
||||||
|
{crop.expectedYield.medium[0]}-{crop.expectedYield.medium[1]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 bg-gray-50 dark:bg-gray-900 px-2 rounded">
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">低适宜性:</span>
|
||||||
|
<span className="font-semibold text-gray-800 dark:text-gray-200">
|
||||||
|
{crop.expectedYield.low[0]}-{crop.expectedYield.low[1]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 风险因子 */}
|
||||||
|
{crop.riskFactors.length > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t">
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-4 h-4 text-amber-600" />
|
||||||
|
主要风险因子
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{crop.riskFactors.map((risk) => (
|
||||||
|
<div
|
||||||
|
key={risk.id}
|
||||||
|
className={`p-3 rounded-lg border ${
|
||||||
|
risk.severity === 'high' ? 'bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800' :
|
||||||
|
risk.severity === 'medium' ? 'bg-amber-50 dark:bg-amber-950 border-amber-200 dark:border-amber-800' :
|
||||||
|
'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<p className={`text-sm font-medium ${
|
||||||
|
risk.severity === 'high' ? 'text-red-900 dark:text-red-100' :
|
||||||
|
risk.severity === 'medium' ? 'text-amber-900 dark:text-amber-100' :
|
||||||
|
'text-blue-900 dark:text-blue-100'
|
||||||
|
}`}>
|
||||||
|
{risk.name}
|
||||||
|
</p>
|
||||||
|
<Badge
|
||||||
|
variant={risk.severity === 'high' ? 'destructive' : 'outline'}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{risk.severity === 'high' ? '高风险' : risk.severity === 'medium' ? '中风险' : '低风险'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
触发条件: {risk.condition}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-foreground">
|
||||||
|
应对建议: {risk.suggestion}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4 border-t mt-4">
|
||||||
|
<Button onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface EvaluationFactor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
weight: number;
|
||||||
|
unit: string;
|
||||||
|
optimalRange: [number, number];
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuitabilityResult {
|
||||||
|
fieldId: string;
|
||||||
|
fieldName: string;
|
||||||
|
totalScore: number;
|
||||||
|
grade: '高度适宜' | '一般适宜' | '不适宜';
|
||||||
|
factors: EvaluationFactor[];
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CropRecommendationState {
|
||||||
|
evaluationResults: SuitabilityResult[];
|
||||||
|
selectedField: string;
|
||||||
|
showKnowledgeBase: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CropRecommendAction =
|
||||||
|
| { type: 'SET_SELECTED_FIELD'; payload: string }
|
||||||
|
| { type: 'SET_SHOW_KNOWLEDGE_BASE'; payload: boolean }
|
||||||
|
| { type: 'SET_EVALUATION_RESULTS'; payload: SuitabilityResult[] }
|
||||||
|
| { type: 'UPDATE_EVALUATION_RESULT'; payload: SuitabilityResult };
|
||||||
|
|
||||||
|
const initialState: CropRecommendationState = {
|
||||||
|
evaluationResults: [
|
||||||
|
{
|
||||||
|
fieldId: 'field-1',
|
||||||
|
fieldName: '东区1号地',
|
||||||
|
totalScore: 87,
|
||||||
|
grade: '高度适宜',
|
||||||
|
factors: [
|
||||||
|
{ id: 'ph', name: 'pH值', value: 6.5, weight: 20, unit: '', optimalRange: [6.0, 7.5], score: 95 },
|
||||||
|
{ id: 'organic', name: '有机质含量', value: 32, weight: 25, unit: 'g/kg', optimalRange: [25, 40], score: 90 },
|
||||||
|
{ id: 'depth', name: '土层厚度', value: 85, weight: 20, unit: 'cm', optimalRange: [60, 100], score: 88 },
|
||||||
|
{ id: 'nitrogen', name: '全氮', value: 1.8, weight: 10, unit: 'g/kg', optimalRange: [1.5, 2.5], score: 85 },
|
||||||
|
{ id: 'phosphorus', name: '全磷', value: 1.2, weight: 10, unit: 'g/kg', optimalRange: [1.0, 2.0], score: 80 },
|
||||||
|
{ id: 'potassium', name: '全钾', value: 18, weight: 10, unit: 'g/kg', optimalRange: [15, 25], score: 82 },
|
||||||
|
{ id: 'drainage', name: '排水性', value: 4, weight: 5, unit: '', optimalRange: [3, 5], score: 90 },
|
||||||
|
],
|
||||||
|
timestamp: '2024-10-15 14:30',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldId: 'field-2',
|
||||||
|
fieldName: '西区2号地',
|
||||||
|
totalScore: 72,
|
||||||
|
grade: '一般适宜',
|
||||||
|
factors: [
|
||||||
|
{ id: 'ph', name: 'pH值', value: 7.8, weight: 20, unit: '', optimalRange: [6.0, 7.5], score: 65 },
|
||||||
|
{ id: 'organic', name: '有机质含量', value: 22, weight: 25, unit: 'g/kg', optimalRange: [25, 40], score: 70 },
|
||||||
|
{ id: 'depth', name: '土层厚度', value: 55, weight: 20, unit: 'cm', optimalRange: [60, 100], score: 68 },
|
||||||
|
{ id: 'nitrogen', name: '全氮', value: 1.3, weight: 10, unit: 'g/kg', optimalRange: [1.5, 2.5], score: 72 },
|
||||||
|
{ id: 'phosphorus', name: '全磷', value: 0.9, weight: 10, unit: 'g/kg', optimalRange: [1.0, 2.0], score: 75 },
|
||||||
|
{ id: 'potassium', name: '全钾', value: 14, weight: 10, unit: 'g/kg', optimalRange: [15, 25], score: 78 },
|
||||||
|
{ id: 'drainage', name: '排水性', value: 3, weight: 5, unit: '', optimalRange: [3, 5], score: 85 },
|
||||||
|
],
|
||||||
|
timestamp: '2024-10-15 14:28',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldId: 'field-3',
|
||||||
|
fieldName: '南区3号地',
|
||||||
|
totalScore: 58,
|
||||||
|
grade: '不适宜',
|
||||||
|
factors: [
|
||||||
|
{ id: 'ph', name: 'pH值', value: 8.5, weight: 20, unit: '', optimalRange: [6.0, 7.5], score: 45 },
|
||||||
|
{ id: 'organic', name: '有机质含量', value: 15, weight: 25, unit: 'g/kg', optimalRange: [25, 40], score: 52 },
|
||||||
|
{ id: 'depth', name: '土层厚度', value: 42, weight: 20, unit: 'cm', optimalRange: [60, 100], score: 55 },
|
||||||
|
{ id: 'nitrogen', name: '全氮', value: 0.8, weight: 10, unit: 'g/kg', optimalRange: [1.5, 2.5], score: 60 },
|
||||||
|
{ id: 'phosphorus', name: '全磷', value: 0.6, weight: 10, unit: 'g/kg', optimalRange: [1.0, 2.0], score: 65 },
|
||||||
|
{ id: 'potassium', name: '全钾', value: 10, weight: 10, unit: 'g/kg', optimalRange: [15, 25], score: 58 },
|
||||||
|
{ id: 'drainage', name: '排水性', value: 2, weight: 5, unit: '', optimalRange: [3, 5], score: 70 },
|
||||||
|
],
|
||||||
|
timestamp: '2024-10-15 14:25',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedField: 'field-1',
|
||||||
|
showKnowledgeBase: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function cropRecommendReducer(
|
||||||
|
state: CropRecommendationState = initialState,
|
||||||
|
action: CropRecommendAction
|
||||||
|
): CropRecommendationState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_SELECTED_FIELD':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedField: action.payload,
|
||||||
|
};
|
||||||
|
case 'SET_SHOW_KNOWLEDGE_BASE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showKnowledgeBase: action.payload,
|
||||||
|
};
|
||||||
|
case 'SET_EVALUATION_RESULTS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
evaluationResults: action.payload,
|
||||||
|
};
|
||||||
|
case 'UPDATE_EVALUATION_RESULT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
evaluationResults: state.evaluationResults.map(result =>
|
||||||
|
result.fieldId === action.payload.fieldId ? action.payload : result
|
||||||
|
),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initialState };
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { BookOpen, Target } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
cropRecommendReducer,
|
||||||
|
initialState,
|
||||||
|
SuitabilityResult
|
||||||
|
} from './components/cropRecommendReducer';
|
||||||
|
import { FieldEnvironmentOverview } from './components/FieldEnvironmentOverview';
|
||||||
|
import { CropRecommendations } from './components/CropRecommendations';
|
||||||
|
import { KnowledgeBaseDialog } from './components/KnowledgeBaseDialog';
|
||||||
|
|
||||||
|
export default function CropPage() {
|
||||||
|
const [state, dispatch] = useReducer(cropRecommendReducer, initialState);
|
||||||
|
|
||||||
|
// 获取当前选中的地块结果
|
||||||
|
const currentResult = state.evaluationResults.find(r => r.fieldId === state.selectedField) || state.evaluationResults[0];
|
||||||
|
|
||||||
|
const handleFieldChange = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_FIELD', payload: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleKnowledgeBase = () => {
|
||||||
|
dispatch({ type: 'SET_SHOW_KNOWLEDGE_BASE', payload: !state.showKnowledgeBase });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
switch (grade) {
|
||||||
|
case '高度适宜': return 'bg-green-500';
|
||||||
|
case '一般适宜': return 'bg-yellow-500';
|
||||||
|
case '不适宜': return 'bg-red-500';
|
||||||
|
default: return 'bg-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-green-800 dark:text-green-200">作物适配推荐</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
智能作物推荐清单、基于知识库的精准匹配
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={handleToggleKnowledgeBase}>
|
||||||
|
<BookOpen className="w-4 h-4 mr-2" />
|
||||||
|
作物知识库
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地块选择和适宜性概览 */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-card rounded-lg border">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">选择地块</label>
|
||||||
|
<Select value={state.selectedField} onValueChange={handleFieldChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{state.evaluationResults.map((result) => (
|
||||||
|
<SelectItem key={result.fieldId} value={result.fieldId}>
|
||||||
|
{result.fieldName} - {result.grade} ({result.totalScore}分)
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 适宜性评分卡片 */}
|
||||||
|
<div className="p-4 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<div className="text-center">
|
||||||
|
<Target className="w-12 h-12 text-green-600 dark:text-green-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">适宜性评分</p>
|
||||||
|
<p className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">{currentResult.totalScore}</p>
|
||||||
|
<Badge className={`${getGradeColor(currentResult.grade)} text-white`}>
|
||||||
|
{currentResult.grade}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 因子评分统计 */}
|
||||||
|
<div className="p-4 bg-card rounded-lg border">
|
||||||
|
<p className="text-sm font-medium mb-3">因子评分统计</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">优秀因子</span>
|
||||||
|
<span className="text-green-600 font-medium">
|
||||||
|
{currentResult.factors.filter(f => f.score >= 80).length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">待改善因子</span>
|
||||||
|
<span className="text-red-600 font-medium">
|
||||||
|
{currentResult.factors.filter(f => f.score < 70).length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">评价时间</span>
|
||||||
|
<span className="text-muted-foreground">{currentResult.timestamp}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-3">
|
||||||
|
<FieldEnvironmentOverview currentResult={currentResult} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 智能作物推荐 */}
|
||||||
|
<CropRecommendations state={state} currentResult={currentResult} />
|
||||||
|
|
||||||
|
{/* 知识库对话框 */}
|
||||||
|
<KnowledgeBaseDialog
|
||||||
|
showKnowledgeBase={state.showKnowledgeBase}
|
||||||
|
onClose={handleToggleKnowledgeBase}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 系统说明 */}
|
||||||
|
<div className="p-5 bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-950 dark:to-blue-950 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
|
||||||
|
<Target className="w-6 h-6 text-green-700 dark:text-green-300 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-green-900 dark:text-green-100 mb-3">作物-环境知识库匹配系统</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm text-green-800 dark:text-green-200">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-2">🌾 知识库构成</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>作物种类</strong>: 多种主要农作物</li>
|
||||||
|
<li>• <strong>土壤参数</strong>: pH、有机质等7项指标</li>
|
||||||
|
<li>• <strong>气候参数</strong>: 温度、降雨、光照</li>
|
||||||
|
<li>• <strong>最佳范围</strong>: 每个因子的要求范围</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-2">🎯 智能匹配流程</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>参数对比</strong>: 地块值与要求对比</li>
|
||||||
|
<li>• <strong>评分计算</strong>: 最佳100分,可接受60分</li>
|
||||||
|
<li>• <strong>综合匹配度</strong>: 加权平均所有因子</li>
|
||||||
|
<li>• <strong>分级推荐</strong>: ≥85高度推荐,70-84推荐</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-2">📊 产量预测机制</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>三级产量区间</strong>: 高/中/低适宜性</li>
|
||||||
|
<li>• <strong>历史数据支撑</strong>: 基于实测数据模型</li>
|
||||||
|
<li>• <strong>动态调整</strong>: 根据匹配度选择区间</li>
|
||||||
|
<li>• <strong>品种差异</strong>: 考虑作物产量特性</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-2">⚠️ 风险识别系统</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>多维度监测</strong>: 土壤、气候、营养</li>
|
||||||
|
<li>• <strong>条件触发</strong>: 自动判断风险存在</li>
|
||||||
|
<li>• <strong>严重度分级</strong>: 高/中/低风险等级</li>
|
||||||
|
<li>• <strong>应对建议</strong>: 每个风险配套措施</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 p-3 bg-white/60 dark:bg-black/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<p className="text-xs text-green-900 dark:text-green-100">
|
||||||
|
<strong>💡 系统优势:</strong>
|
||||||
|
本系统整合了土壤科学、作物学、气象学等多学科知识,建立了完整的作物-环境适配知识库。
|
||||||
|
通过科学的评分体系和智能匹配算法,为农业生产决策提供可靠的数据支持,帮助优化作物布局,
|
||||||
|
提高土地利用率和经济效益,同时有效规避种植风险。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function WeightPage() {
|
|
||||||
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> /land-information/suitability/weight
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -318,17 +318,17 @@ const fieldMessageManagement = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "多因子综合评价",
|
title: "多因子综合评价",
|
||||||
url: "/land-information/suitability/comprehensive",
|
url: "/land-information/suitability/multiFactor",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "自动化空间分析",
|
title: "自动化空间分析",
|
||||||
url: "/land-information/suitability/batch",
|
url: "/land-information/suitability/auto",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "作物适配推荐",
|
title: "作物适配推荐",
|
||||||
url: "/land-information/suitability/crop",
|
url: "/land-information/suitability/recommend",
|
||||||
isActive: false
|
isActive: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
415
crop-x/temp_file.tsx
Normal file
415
crop-x/temp_file.tsx
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
/**
|
||||||
|
* 多因子综合评价组件
|
||||||
|
* 提供地块适宜性评价、权重配置、批量分析和作物推荐功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Leaf,
|
||||||
|
TrendingUp,
|
||||||
|
Award,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle2,
|
||||||
|
Play,
|
||||||
|
Settings,
|
||||||
|
Download,
|
||||||
|
Eye,
|
||||||
|
Calculator,
|
||||||
|
Database,
|
||||||
|
RefreshCw,
|
||||||
|
Zap,
|
||||||
|
Target,
|
||||||
|
Droplet,
|
||||||
|
Cloud,
|
||||||
|
Sun,
|
||||||
|
ThermometerSun,
|
||||||
|
BookOpen,
|
||||||
|
Beaker,
|
||||||
|
Info,
|
||||||
|
BarChart3,
|
||||||
|
Filter
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EvaluationFactor,
|
||||||
|
SuitabilityResult,
|
||||||
|
FactorWeight,
|
||||||
|
BatchProgress,
|
||||||
|
getGradeColor,
|
||||||
|
getScoreColor,
|
||||||
|
getSuitabilityLevelColor,
|
||||||
|
formatDate,
|
||||||
|
MOCK_FIELDS
|
||||||
|
} from './multiFactorTypes';
|
||||||
|
import {
|
||||||
|
MultiFactorService
|
||||||
|
} from './multiFactorService';
|
||||||
|
import {
|
||||||
|
matchCropsForField,
|
||||||
|
cropKnowledgeBase
|
||||||
|
} from './cropKnowledgeBase';
|
||||||
|
|
||||||
|
export function MultiFactorEvaluation() {
|
||||||
|
const [selectedField, setSelectedField] = useState('field-1');
|
||||||
|
const [showWeightConfig, setShowWeightConfig] = useState(false);
|
||||||
|
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
|
||||||
|
const [batchProgress, setBatchProgress] = useState<BatchProgress>({
|
||||||
|
total: 0,
|
||||||
|
processed: 0,
|
||||||
|
highSuitability: 0,
|
||||||
|
mediumSuitability: 0,
|
||||||
|
lowSuitability: 0,
|
||||||
|
currentField: '',
|
||||||
|
});
|
||||||
|
const [isBatchRunning, setIsBatchRunning] = useState(false);
|
||||||
|
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
|
||||||
|
|
||||||
|
// 评价因子权重配置
|
||||||
|
const [factorWeights, setFactorWeights] = useState<FactorWeight[]>([
|
||||||
|
{ id: 'ph', name: 'pH值', weight: 20, unit: '' },
|
||||||
|
{ id: 'organic', name: '有机质含量', weight: 25, unit: 'g/kg' },
|
||||||
|
{ id: 'depth', name: '土层厚度', weight: 20, unit: 'cm' },
|
||||||
|
{ id: 'nitrogen', name: '全氮', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'phosphorus', name: '全磷', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'potassium', name: '全钾', weight: 10, unit: 'g/kg' },
|
||||||
|
{ id: 'drainage', name: '排水性', weight: 5, unit: '' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 模拟适宜性评价结果
|
||||||
|
const [evaluationResults, setEvaluationResults] = useState<SuitabilityResult[]>(
|
||||||
|
MultiFactorService.generateMockEvaluationData()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取当前选中的地块结果
|
||||||
|
const currentResult =
|
||||||
|
evaluationResults.find(r => r.fieldId === selectedField) ||
|
||||||
|
batchAnalysisResults.find(r => r.fieldId === selectedField) ||
|
||||||
|
evaluationResults[0];
|
||||||
|
|
||||||
|
// 批量分析处理函数
|
||||||
|
const handleRunBatchAnalysis = async () => {
|
||||||
|
const validation = MultiFactorService.validateWeights(factorWeights);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
toast.error(`权重总和必须为100%才能进行批量分析(当前:${validation.totalWeight}%)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsBatchRunning(true);
|
||||||
|
setBatchProgress({
|
||||||
|
total: 68,
|
||||||
|
processed: 0,
|
||||||
|
highSuitability: 0,
|
||||||
|
mediumSuitability: 0,
|
||||||
|
lowSuitability: 0,
|
||||||
|
currentField: '',
|
||||||
|
});
|
||||||
|
setBatchAnalysisResults([]);
|
||||||
|
|
||||||
|
toast.success('开始批量分析,正在读取地块数据...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await MultiFactorService.runBatchAnalysis(
|
||||||
|
factorWeights,
|
||||||
|
(progress) => {
|
||||||
|
setBatchProgress(progress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setBatchAnalysisResults(results);
|
||||||
|
setIsBatchRunning(false);
|
||||||
|
toast.success(`批量分析完成!已为${results.length}个地块生成适宜性评价结果`);
|
||||||
|
} catch (error) {
|
||||||
|
setIsBatchRunning(false);
|
||||||
|
toast.error('批量分析失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateWeight = (id: string, newWeight: number) => {
|
||||||
|
setFactorWeights(prev =>
|
||||||
|
MultiFactorService.updateWeight(prev, id, newWeight)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetWeights = () => {
|
||||||
|
setFactorWeights(MultiFactorService.resetWeights());
|
||||||
|
toast.success('权重已恢复默认值');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplyPreset = (preset: 'grain' | 'economic' | 'default') => {
|
||||||
|
setFactorWeights(MultiFactorService.getWeightPreset(preset));
|
||||||
|
const presetName = preset === 'grain' ? '粮食作物' : preset === 'economic' ? '经济作物' : '默认';
|
||||||
|
toast.success(`已应用${presetName}权重方案`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
|
||||||
|
|
||||||
|
// 执行地块适宜性评价
|
||||||
|
const handleEvaluateField = () => {
|
||||||
|
const validation = MultiFactorService.validateWeights(factorWeights);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
toast.error(`权重总和必须为100%才能进行评价(当前:${validation.totalWeight}%)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = MultiFactorService.generateEvaluationResult(selectedField, factorWeights);
|
||||||
|
setEvaluationResults(prev =>
|
||||||
|
prev.map(r => r.fieldId === selectedField ? result : r)
|
||||||
|
);
|
||||||
|
toast.success('评价完成!已应用当前权重配置计算综合得分');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('评价失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportResults = () => {
|
||||||
|
const resultsToExport = batchAnalysisResults.length > 0 ? batchAnalysisResults : evaluationResults;
|
||||||
|
toast.success('正在导出评价结果...');
|
||||||
|
// 模拟导出功能
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.success(`已导出${resultsToExport.length}个地块的评价结果`);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-green-800 dark:text-green-200">多因子综合评价</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
地块适宜性评价、自动化空间分析与作物适配推荐
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setShowKnowledgeBase(true)}>
|
||||||
|
<BookOpen className="w-4 h-4 mr-2" />
|
||||||
|
作物知识库
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => setShowWeightConfig(true)}>
|
||||||
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
|
权重配置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 多因子综合评价 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">选择地块</label>
|
||||||
|
<Select value={selectedField} onValueChange={setSelectedField}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{evaluationResults.map((result) => (
|
||||||
|
<SelectItem key={result.fieldId} value={result.fieldId}>
|
||||||
|
{result.fieldName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 items-end">
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={handleEvaluateField}
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
开始评价
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 评价结果总览 */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<Card className="p-6 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900">
|
||||||
|
<div className="text-center">
|
||||||
|
<Award className="w-12 h-12 text-green-600 dark:text-green-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">综合评分</p>
|
||||||
|
<p
|
||||||
|
className="text-4xl mb-2"
|
||||||
|
style={{ color: getGradeColor(currentResult.grade) }}
|
||||||
|
>
|
||||||
|
{currentResult.totalScore}
|
||||||
|
</p>
|
||||||
|
<Badge
|
||||||
|
className="text-white font-light"
|
||||||
|
style={{ backgroundColor: getGradeColor(currentResult.grade) }}
|
||||||
|
>
|
||||||
|
{currentResult.grade}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<CheckCircle2 className="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">优秀因子</p>
|
||||||
|
<p className="text-4xl text-blue-600 dark:text-blue-400 mb-2">
|
||||||
|
{currentResult.factors.filter(f => f.score >= 80).length}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
/ {currentResult.factors.length} 项
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<AlertTriangle className="w-12 h-12 text-yellow-600 dark:text-yellow-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">待改善因子</p>
|
||||||
|
<p className="text-4xl text-yellow-600 dark:text-yellow-400 mb-2">
|
||||||
|
{currentResult.factors.filter(f => f.score < 70).length}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">需要优化</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<TrendingUp className="w-12 h-12 text-purple-600 dark:text-purple-400 mx-auto mb-3" />
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">评价时间</p>
|
||||||
|
<p className="text-sm text-purple-600 dark:text-purple-400 mb-2">
|
||||||
|
{formatDate(currentResult.timestamp)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">最近更新</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 因子详细评分 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">评价因子详细分析</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentResult.factors.map((factor) => (
|
||||||
|
<div key={factor.id} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm font-medium">{factor.name}</span>
|
||||||
|
<Badge variant="outline" className="text-xs font-light">
|
||||||
|
权重: {factor.weight}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
实际值: {factor.value.toFixed(1)}{factor.unit}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
最佳范围: {factor.optimalRange[0]}-{factor.optimalRange[1]}{factor.unit}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-sm font-medium"
|
||||||
|
style={{ color: getScoreColor(factor.score) }}
|
||||||
|
>
|
||||||
|
{factor.score}分
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Progress value={factor.score} className="h-2" />
|
||||||
|
<div className="absolute top-0 left-0 h-2 w-full flex items-center">
|
||||||
|
<div
|
||||||
|
className="absolute h-3 w-1 bg-blue-500"
|
||||||
|
style={{
|
||||||
|
left: `${((factor.optimalRange[0] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute h-3 w-1 bg-blue-500"
|
||||||
|
style={{
|
||||||
|
left: `${((factor.optimalRange[1] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 加权计算说明 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">层次分析法(AHP)加权计算</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-900 dark:text-blue-100 mb-2">计算公式:</p>
|
||||||
|
<code className="text-xs text-blue-800 dark:text-blue-200 block mb-2">
|
||||||
|
总分 = Σ(因子得分 × 因子权重)
|
||||||
|
</code>
|
||||||
|
<p className="text-xs text-blue-800 dark:text-blue-200">
|
||||||
|
= ({currentResult.factors.map((f, i) =>
|
||||||
|
`${f.score} × ${f.weight}%${i < currentResult.factors.length - 1 ? ' + ' : ''}`
|
||||||
|
).join('')})
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-blue-800 dark:text-blue-200 mt-2">
|
||||||
|
= {currentResult.totalScore}分
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<h4 className="text-green-900 dark:text-green-100 mb-2">高度适宜 (≥80分)</h4>
|
||||||
|
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
|
||||||
|
<li>• 土壤条件优秀</li>
|
||||||
|
<li>• 适合多种作物</li>
|
||||||
|
<li>• 预期产量高</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
|
||||||
|
<h4 className="text-yellow-900 dark:text-yellow-100 mb-2">一般适宜 (60-79分)</h4>
|
||||||
|
<ul className="text-sm text-yellow-800 dark:text-yellow-200 space-y-1">
|
||||||
|
<li>• 部分因子需改善</li>
|
||||||
|
<li>• 可种植耐性作物</li>
|
||||||
|
<li>• 需适当改良</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-950 rounded-lg">
|
||||||
|
<h4 className="text-red-900 dark:text-red-100 mb-2">不适宜 (<60分)</h4>
|
||||||
|
<ul className="text-sm text-red-800 dark:text-red-200 space-y-1">
|
||||||
|
<li>• 土壤条件较差</li>
|
||||||
|
<li>• 需大幅改良</li>
|
||||||
|
<li>• 风险较高</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 多因子综合评价功能说明 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Zap className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="mb-2">多因子综合评价功能说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>关键指标整合</strong>: 整合pH值、有机质含量、土层厚度、全氮、全磷、全钾、排水性等7项关键土壤指标</li>
|
||||||
|
<li>• <strong>层次分析法(AHP)</strong>: 采用层次分析法构建加权评分体系,公式:总分 = Σ(因子得分 × 因子权重)</li>
|
||||||
|
<li>• <strong>单因子评分</strong>: 根据实际值与最佳范围的接近程度计算各因子得分(0-100分),范围内得分85-100分</li>
|
||||||
|
<li>• <strong>自定义权重</strong>: 支持手动调整各指标权重,提供粮食作物和经济作物两种预设方案</li>
|
||||||
|
<li>• <strong>综合得分</strong>: 输出0-100分的适宜性评分,自动计算加权总分</li>
|
||||||
|
<li>• <strong>三级分级</strong>: 高度适宜(≥80分)、一般适宜(60-79分)、不适宜(<60分)</li>
|
||||||
|
<li>• <strong>可视化展示</strong>: 提供进度条、颜色标识等多维度可视化,直观展示各因子评分与最佳范围对比</li>
|
||||||
|
<li>• <strong>改善建议</strong>: 自动识别待改善因子,为土壤改良提供决策依据</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
Reference in New Issue
Block a user