diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/FieldSelector.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/FieldSelector.tsx new file mode 100644 index 0000000..f578b22 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/FieldSelector.tsx @@ -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 ( + +
+
+ +
+ {comparisonFields.map(field => ( + + {field.name} + + + ))} + {availableFields.length > 0 && ( + + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/MapComparison.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/MapComparison.tsx new file mode 100644 index 0000000..81488c6 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/MapComparison.tsx @@ -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 ( + +

+ + 地块空间分布对比 +

+
+
+ +

请先选择要对比的地块

+

地图将显示各地块的空间分布位置

+
+
+
+ ); + } + + return ( + +

+ + 地块空间分布对比 +

+
+ {comparisonFields.map((field, index) => ( +
+

{field.name}

+
+
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+
+ +

{field.location}

+ + {field.suitabilityGrade} + +
+
+
+ ))} +
+ + ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/NutrientComparison.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/NutrientComparison.tsx new file mode 100644 index 0000000..966002a --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/NutrientComparison.tsx @@ -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 ( + +

+ + 土壤养分对比 (g/kg) +

+
+
+ +

请选择地块

+
+
+
+ ); + } + + return ( + +

+ + 土壤养分对比 (g/kg) +

+
+ + + + + + + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/RadarChart.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/RadarChart.tsx new file mode 100644 index 0000000..0bac777 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/RadarChart.tsx @@ -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 ( + +

+ + 多维度雷达图对比 +

+

+ 综合展示各地块在多个指标上的相对优劣势 +

+
+
+ +

请先选择要对比的地块

+

雷达图将显示各地块的多维度指标对比

+
+
+
+ ); + } + + return ( + +

+ + 多维度雷达图对比 +

+

+ 综合展示各地块在多个指标上的相对优劣势 +

+
+ + + + + + {comparisonFields.map((field, index) => ( + + ))} + + + +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/YieldComparison.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/YieldComparison.tsx new file mode 100644 index 0000000..de9e7f8 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/YieldComparison.tsx @@ -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 ( +
+ +

+ + 产量对比 (kg/亩) +

+
+
+ +

请选择地块

+
+
+
+ + +

+ + 有机质含量对比 (g/kg) +

+
+
+ +

请选择地块

+
+
+
+
+ ); + } + + return ( +
+ +

+ + 产量对比 (kg/亩) +

+
+ + + + + + + + + + +
+
+ + +

+ + 有机质含量对比 (g/kg) +

+
+ + + + + + + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/chartAnalysisReducer.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/chartAnalysisReducer.tsx new file mode 100644 index 0000000..966c3ba --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/chartAnalysisReducer.tsx @@ -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, + }; +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/page.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/page.tsx index 3bb7278..0d81786 100644 --- a/crop-x/src/app/(app)/land-information/comparison/chart/page.tsx +++ b/crop-x/src/app/(app)/land-information/comparison/chart/page.tsx @@ -1,18 +1,158 @@ 'use client'; +import { useEffect } from 'react'; 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() { + const { setFields, state } = useChartAnalysis(); + + // 初始化数据 + useEffect(() => { + // 直接使用模拟数据,确保数据可用 + setFields(mockFieldsData); + localStorage.setItem('chart-analysis-fields', JSON.stringify(mockFieldsData)); + }, [setFields]); + + // 如果数据还没有加载,显示loading状态 + if (state.fields.length === 0) { + return ( +
+
+
+

可视化图表分析

+

+ 多维度指标可视化展示与对比分析 +

+
+
+ +
+

正在加载数据...

+
+
+
+ ); + } + return (
- -

图表对比

-
-

- 页面路径: /land-information/comparison/chart +

+
+

可视化图表分析

+

+ 多维度指标可视化展示与对比分析

- +
+ + {/* 地块选择器 */} + + + {/* 图表分析区域 */} +
+ {/* 雷达图 */} + + + {/* 产量与有机质对比 */} + + + {/* 养分对比 */} + + + {/* 地图对比 */} + +
); } \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/indicator/components/BasicProperties.tsx b/crop-x/src/app/(app)/land-information/comparison/indicator/components/BasicProperties.tsx new file mode 100644 index 0000000..97168c1 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/indicator/components/BasicProperties.tsx @@ -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 ( + +
+ +

基础属性对比

+
+
+ + + + + {comparisonFields.map(field => ( + + ))} + + + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + +
指标项 + {field.name} +
面积 (亩) + {field.area} +
位置 + {field.location} +
土壤类型 + {field.soilType} +
坡度 (°) + + {field.slope} + +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/indicator/components/FieldSelector.tsx b/crop-x/src/app/(app)/land-information/comparison/indicator/components/FieldSelector.tsx new file mode 100644 index 0000000..babebff --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/indicator/components/FieldSelector.tsx @@ -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 ( + +
+
+ +
+ {comparisonFields.map(field => ( + + {field.name} + + + ))} + {availableFields.length > 0 && ( + + )} +
+ {selectedFields.length < 2 && ( +

+ 至少需要选择2个地块进行对比 +

+ )} + {selectedFields.length >= 4 && ( +

+ 最多只能同时对比4个地块 +

+ )} + + {/* 选中地块的简要信息 */} + {comparisonFields.length > 0 && ( +
+

已选地块信息:

+
+ {comparisonFields.map(field => ( +
+
+ {field.name} + + {field.suitabilityGrade} + +
+
+
+ 面积: + {field.area}亩 +
+
+ 作物: + {field.currentCrop} +
+
+ 产量: + {field.yield}kg/亩 +
+
+ 评分: + {field.suitabilityScore}分 +
+
+
+ ))} +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/indicator/components/ManagementStatus.tsx b/crop-x/src/app/(app)/land-information/comparison/indicator/components/ManagementStatus.tsx new file mode 100644 index 0000000..11e8446 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/indicator/components/ManagementStatus.tsx @@ -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 ( + +
+ +

经营现状对比

+
+
+ + + + + {comparisonFields.map(field => ( + + ))} + + + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + +
指标项 + {field.name} +
当前作物 + {field.currentCrop} +
产量 (kg/亩) + + {field.yield} + +
+ {field.yield >= 700 ? '高产' : field.yield >= 500 ? '中产' : '低产'} +
+
灌溉方式 + + {field.irrigation} + +
排水状况 + + {field.drainage} + +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/indicator/components/NaturalConditions.tsx b/crop-x/src/app/(app)/land-information/comparison/indicator/components/NaturalConditions.tsx new file mode 100644 index 0000000..a541daf --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/indicator/components/NaturalConditions.tsx @@ -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 ( + +
+ +

自然条件对比

+
+
+ + + + + {comparisonFields.map(field => ( + + ))} + + + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + +
指标项 + {field.name} +
pH值 +
+ {field.ph} + {getPhStatus(field.ph) === 'optimal' ? ( + + ) : ( + + )} +
+
+ 最佳: 6.0-7.5 +
+
有机质 (g/kg) +
+ {field.organicMatter} + {getOrganicStatus(field.organicMatter) === 'optimal' ? ( + + ) : ( + + )} +
+
+ 最佳: ≥25 +
+
全氮 (g/kg) + = 1.5 ? 'text-green-600' : field.nitrogen >= 1.0 ? 'text-yellow-600' : 'text-red-600'}> + {field.nitrogen} + +
+ 最佳: ≥1.5 +
+
全磷 (g/kg) + = 1.0 ? 'text-green-600' : field.phosphorus >= 0.6 ? 'text-yellow-600' : 'text-red-600'}> + {field.phosphorus} + +
+ 最佳: ≥1.0 +
+
全钾 (g/kg) + = 15 ? 'text-green-600' : field.potassium >= 10 ? 'text-yellow-600' : 'text-red-600'}> + {field.potassium} + +
+ 最佳: ≥15 +
+
土层厚度 (cm) +
+ {field.soilDepth} + {getDepthStatus(field.soilDepth) === 'optimal' ? ( + + ) : ( + + )} +
+
+ 最佳: ≥60 +
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/indicator/components/SuitabilityEvaluation.tsx b/crop-x/src/app/(app)/land-information/comparison/indicator/components/SuitabilityEvaluation.tsx new file mode 100644 index 0000000..9817bee --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/indicator/components/SuitabilityEvaluation.tsx @@ -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 ( + +
+ +

适宜性评价对比

+
+
+ + + + + {comparisonFields.map(field => ( + + ))} + + + + + + {comparisonFields.map(field => ( + + ))} + + + + {comparisonFields.map(field => ( + + ))} + + + + {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 ( + + ); + })} + + + + {comparisonFields.map(field => ( + + ))} + + +
指标项 + {field.name} +
综合评分 +
+ + {field.suitabilityScore} + +
+ {getScoreLevel(field.suitabilityScore)} +
+
+
适宜性等级 +
+ + {field.suitabilityGrade} + +
+ {field.suitabilityGrade === '高度适宜' ? '适合多种作物' : + field.suitabilityGrade === '一般适宜' ? '适合耐性作物' : '需系统改良'} +
+
+
主要限制因子 + {limitations.length > 0 ? ( +
+ {limitations.slice(0, 2).map((limit, index) => ( + + {limit} + + ))} + {limitations.length > 2 && ( + + +{limitations.length - 2}个 + + )} +
+ ) : ( + 无限制因子 + )} +
推荐作物类型 +
+ {field.suitabilityGrade === '高度适宜' ? ( + <> + + 经济作物 + + + 粮食作物 + + + ) : field.suitabilityGrade === '一般适宜' ? ( + + 耐性作物 + + ) : ( + + 需改良后种植 + + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/indicator/components/multiDimensionReducer.tsx b/crop-x/src/app/(app)/land-information/comparison/indicator/components/multiDimensionReducer.tsx new file mode 100644 index 0000000..572ce5e --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/indicator/components/multiDimensionReducer.tsx @@ -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; + } +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/indicator/page.tsx b/crop-x/src/app/(app)/land-information/comparison/indicator/page.tsx index 5456ea4..99773d7 100644 --- a/crop-x/src/app/(app)/land-information/comparison/indicator/page.tsx +++ b/crop-x/src/app/(app)/land-information/comparison/indicator/page.tsx @@ -1,18 +1,235 @@ '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 (
- -

指标对比

-
-

- 页面路径: /land-information/comparison/indicator +

+
+

多维度指标看板

+

+ 地块基础属性、自然条件、经营现状与适宜性评价的全面对比分析

- +
+ + {/* 地块选择器 */} + + + {/* 统计概览 */} + {statistics && ( +
+
+
+

对比地块总面积

+

+ {statistics.totalArea} +

+

+
+
+ +
+
+

平均产量

+

+ {statistics.avgYield} +

+

kg/亩

+
+
+ +
+
+

平均适宜性评分

+

+ {statistics.avgScore} +

+

+
+
+ +
+
+

平均有机质

+

+ {statistics.avgOrganic} +

+

g/kg

+
+
+
+ )} + + {/* 适宜性分布统计 */} + {statistics && ( +
+
+
+
+

高度适宜

+

+ {statistics.highSuitabilityCount} +

+
+
📊
+
+
+ +
+
+
+

一般适宜

+

+ {statistics.mediumSuitabilityCount} +

+
+
📈
+
+
+ +
+
+
+

不适宜

+

+ {statistics.lowSuitabilityCount} +

+
+
⚠️
+
+
+
+ )} + + {/* 详细对比表格 */} + {comparisonFields.length > 0 && ( +
+ {/* 基础属性对比 */} + + + {/* 自然条件对比 */} + + + {/* 经营现状对比 */} + + + {/* 适宜性评价对比 */} + +
+ )} + + {/* 优秀地块展示 */} + {statistics && comparisonFields.length > 0 && ( +
+
+

🏆 最优地块

+
+

{statistics.bestField.name}

+

综合评分: {statistics.bestField.suitabilityScore}分

+

产量: {statistics.bestField.yield}kg/亩

+
+
+ +
+

🌾 产量冠军

+
+

{statistics.highestYieldField.name}

+

产量: {statistics.highestYieldField.yield}kg/亩

+

评分: {statistics.highestYieldField.suitabilityScore}分

+
+
+ +
+

🌱 肥力最佳

+
+

{statistics.bestOrganicField.name}

+

有机质: {statistics.bestOrganicField.organicMatter}g/kg

+

评分: {statistics.bestOrganicField.suitabilityScore}分

+
+
+
+ )} + + {/* 空状态 */} + {comparisonFields.length === 0 && ( +
+
+

暂无对比地块

+

+ 请先选择2-4个地块进行多维度指标对比分析 +

+
+
+ )}
); } \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/report/components/FieldSelector.tsx b/crop-x/src/app/(app)/land-information/comparison/report/components/FieldSelector.tsx new file mode 100644 index 0000000..65f93f2 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/report/components/FieldSelector.tsx @@ -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 ( + +
+
+ +
+ {comparisonFields.map(field => ( + + {field.name} + + + ))} + {availableFields.length > 0 && ( + + )} +
+
+
+ + +
+
+ + {state.reportGenerating && ( +
+
+ 生成进度 + {state.reportProgress}% +
+ +

+ 正在生成报告,请稍候... +

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/report/components/ReportList.tsx b/crop-x/src/app/(app)/land-information/comparison/report/components/ReportList.tsx new file mode 100644 index 0000000..151865d --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/report/components/ReportList.tsx @@ -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 ( + +

+ + 历史报告列表 +

+ + {savedReports.length === 0 ? ( +
+ +

暂无保存的报告

+

选择地块后点击"生成报告"创建新报告

+
+ ) : ( +
+ {savedReports.map((report) => ( +
+
+
+

+ + {report.name} +

+
+ + + 生成时间: {report.generatedTime} + + + + 对比地块: {report.comparedFields.join('、')} + +
+
+
+ + + +
+
+
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/report/components/ReportPreview.tsx b/crop-x/src/app/(app)/land-information/comparison/report/components/ReportPreview.tsx new file mode 100644 index 0000000..d040245 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/report/components/ReportPreview.tsx @@ -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 ( +
+ {/* 报告预览 */} + +

+ + 当前对比分析报告预览 +

+ + {/* 报告标题 */} +
+

地块对比分析报告

+

+ 预览时间: {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('、')} +

+
+ + {/* 执行摘要 */} +
+

+ + 执行摘要 +

+
+
+

最优地块

+

+ {[...comparisonFields].sort((a, b) => b.suitabilityScore - a.suitabilityScore)[0].name} +

+

+ 综合评分: {[...comparisonFields].sort((a, b) => b.suitabilityScore - a.suitabilityScore)[0].suitabilityScore} +

+
+
+

最高产量

+

+ {[...comparisonFields].sort((a, b) => b.yield - a.yield)[0].name} +

+

+ {[...comparisonFields].sort((a, b) => b.yield - a.yield)[0].yield} kg/亩 +

+
+
+

最佳有机质

+

+ {[...comparisonFields].sort((a, b) => b.organicMatter - a.organicMatter)[0].name} +

+

+ {[...comparisonFields].sort((a, b) => b.organicMatter - a.organicMatter)[0].organicMatter} g/kg +

+
+
+
+ + {/* 智能分析结论 */} +
+

+ + 智能分析结论 +

+
+ {smartAnalyses.map((analysis, index) => ( +
+

+ + {analysis} +

+
+ ))} +
+
+ + {/* 改进建议 */} +
+

+ + 改进建议 +

+
+ {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 ( +
+
{field.name}
+
    + {suggestions.map((suggestion, i) => ( +
  • + + {suggestion} +
  • + ))} +
+
+ ); + })} +
+
+ + {/* 数据表格 */} +
+

详细数据表格

+
+ + + + + + + + + + + + + {comparisonFields.map(field => ( + + + + + + + + + ))} + +
地块名称面积pH有机质产量适宜性
{field.name}{field.area}亩{field.ph}{field.organicMatter} g/kg{field.yield} kg/亩 + + {field.suitabilityGrade} + +
+
+
+ + {/* 图表插入位置 */} +
+

+ 📊 报告中将自动插入雷达图、柱状图等可视化图表 +

+
+
+ + {/* 下载当前报告 */} + +
+
+

下载当前对比报告

+

+ 将当前预览的报告导出为PDF或Word格式 +

+
+
+ +
+ + +
+ +
+

+ 💡 报告内容包含: +

+
    +
  • • 执行摘要与关键指标对比
  • +
  • • 多维度指标对比表格
  • +
  • • 雷达图、柱状图等可视化图表
  • +
  • • 基于规则引擎的智能分析结论
  • +
  • • 针对性改进建议与措施
  • +
  • • 地块空间分布对比图
  • +
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/report/components/reportComparisonReducer.tsx b/crop-x/src/app/(app)/land-information/comparison/report/components/reportComparisonReducer.tsx new file mode 100644 index 0000000..147cd80 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/report/components/reportComparisonReducer.tsx @@ -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 }; \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/report/page.tsx b/crop-x/src/app/(app)/land-information/comparison/report/page.tsx index 92b6db7..31d73c4 100644 --- a/crop-x/src/app/(app)/land-information/comparison/report/page.tsx +++ b/crop-x/src/app/(app)/land-information/comparison/report/page.tsx @@ -1,18 +1,307 @@ '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() { + 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 = ` + + + + + + + +

地块对比分析报告

+ +
+

生成时间:${reportData.generatedTime}

+

对比地块:${reportData.comparedFields.join('、')}

+
+ +

执行摘要

+
+
+ 最优地块:${bestField?.name || 'N/A'} + (综合评分: ${bestField?.suitabilityScore || 0}) +
+
+ 最高产量:${highestYield?.name || 'N/A'} + (${highestYield?.yield || 0} kg/亩) +
+
+ 最佳有机质:${bestOrganic?.name || 'N/A'} + (${bestOrganic?.organicMatter || 0} g/kg) +
+
+ +

详细数据对比

+ + + + + + + + + + + + + + ${fields.map(field => ` + + + + + + + + + + `).join('')} + +
地块名称面积(亩)土壤类型pH值有机质(g/kg)产量(kg/亩)适宜性
${field.name}${field.area}${field.soilType}${field.ph}${field.organicMatter}${field.yield}${field.suitabilityGrade}
+ +

智能分析结论

+
+

• 根据对比分析,${bestField?.name}综合适宜性最高,建议优先发展。

+

${highestYield?.name}产量表现最优,可作为高产示范田。

+

${bestOrganic?.name}有机质含量最佳,土壤肥力较好。

+
+ + + + + `; + + 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 (
- -

报告对比

-
-

- 页面路径: /land-information/comparison/report +

+
+

报告对比生成

+

+ 智能报告生成、多维度对比分析与一键导出

- +
+ +
+
+ + {/* 地块选择器 */} + + + {/* 历史报告列表 */} + + + {/* 报告预览 */} + handleDownloadPDF()} + onDownloadWord={() => handleDownloadWord()} + />
); } \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisCharts.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisCharts.tsx new file mode 100644 index 0000000..0ffb2b6 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisCharts.tsx @@ -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; +} + +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 ( + + +

暂无图表数据

+

+ 请先运行分析生成结果 +

+
+ ); + } + + return ( +
+ + + + + 得分分布 + + + + 因子分析 + + + + 作物推荐 + + + + 经济效益 + + + + 对比分析 + + + + {/* 得分分布 */} + +
+ +

得分分布直方图

+ + + + + + + + {scoreDistributionData.map((entry, index) => ( + + ))} + + + +
+ + +

等级分布饼图

+ + + `${range}: ${count}个`} + outerRadius={80} + fill="#8884d8" + dataKey="count" + > + {scoreDistributionData.map((entry, index) => ( + + ))} + + + + +
+
+
+ + {/* 因子分析 */} + +
+ +

评价因子权重分布

+ + + + + + + + + +
+ + +

各维度得分对比

+ + + + + + + + + + +
+
+
+ + {/* 作物推荐 */} + +
+ +

作物推荐频率

+ + + + + + + + + +
+ + +

作物适宜性得分

+ + + + + + + + + +
+
+
+ + {/* 经济效益 */} + +
+ +

地块亩均收益

+ + + + + + + + + +
+ + +

得分与收益相关性

+ + a.score - b.score)}> + + + + + + + +
+
+
+ + {/* 对比分析 */} + + +

地块综合对比

+ + ({ + name: result.blockName, + 综合得分: result.overallScore, + 土壤得分: Math.round(result.soilScore), + 气候得分: Math.round(result.climateScore), + 地形得分: Math.round(result.topographyScore), + 基础设施得分: Math.round(result.infrastructureScore) + }))}> + + + + + + + + + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisControlPanel.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisControlPanel.tsx new file mode 100644 index 0000000..5613854 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisControlPanel.tsx @@ -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; +} + +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 ( +
+ {/* 分析状态 */} + +

+ + 分析状态 +

+ +
+
+ 选择地块 + 0 ? 'default' : 'secondary'}> + {state.selectedBlocks.length} / {state.landBlocks.length} + +
+ +
+ 评价因子 + + {enabledFactorsCount} / {state.factors.length} + +
+ +
+ 分析结果 + 0 ? 'default' : 'secondary'}> + {state.analysisResults.length} 个 + +
+
+
+ + {/* 权重配置 */} + +

+ + 权重配置 +

+ +
+
+
+ 土壤条件 + {Math.round(state.weightConfig.soil * 100)}% +
+ +
+ +
+
+ 气候条件 + {Math.round(state.weightConfig.climate * 100)}% +
+ +
+ +
+
+ 地形条件 + {Math.round(state.weightConfig.topography * 100)}% +
+ +
+ +
+
+ 基础设施 + {Math.round(state.weightConfig.infrastructure * 100)}% +
+ +
+
+ +
+ + 总权重: {Math.round(totalWeight * 100)}% + {Math.abs(totalWeight - 1) > 0.01 && ( + (需调整为100%) + )} + +
+ + +
+ + {/* 风险评估 */} + +

+ + 风险评估 +

+ +
+
+ 综合风险等级 + + {riskLevel.text} + +
+ + {state.analysisResults.length > 0 && ( + <> +
+
+ {Math.round( + state.analysisResults.reduce((sum, r) => sum + r.overallScore, 0) / + state.analysisResults.length + )}分 +
+
平均适宜性得分
+
+ +
+
+ 优秀地块 (≥80分) + + {state.analysisResults.filter(r => r.overallScore >= 80).length} 个 + +
+
+ 良好地块 (60-79分) + + {state.analysisResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length} 个 + +
+
+ 需改进 (<60分) + + {state.analysisResults.filter(r => r.overallScore < 60).length} 个 + +
+
+ + )} +
+
+ + {/* 快速操作 */} + +

+ + 快速操作 +

+ +
+ + + + + +
+
+ + {/* 分析建议 */} + {state.analysisResults.length > 0 && ( + +

+ + 智能建议 +

+ +
+
+

优先推荐作物

+

+ {(() => { + 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('、'); + })()} +

+
+ +
+

最佳种植地块

+

+ {state.analysisResults + .sort((a, b) => b.overallScore - a.overallScore) + .slice(0, 2) + .map(r => r.blockName) + .join('、')} +

+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisReport.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisReport.tsx new file mode 100644 index 0000000..fc80c43 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisReport.tsx @@ -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; +} + +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 = ` + + + + + + ${report.title} + + + +
+

${report.title}

+

生成时间: ${report.createdAt}

+
+ +
+

执行摘要

+
+
+

分析概览

+

地块数量: ${report.summary.totalBlocks} 个

+

总面积: ${report.summary.totalArea} 亩

+

平均得分: ${report.summary.averageScore} 分

+
+
+

经济效益

+

预估产量: ${report.summary.estimatedProduction.toLocaleString()} kg

+

预估收益: ¥${report.summary.estimatedRevenue.toLocaleString()}

+

亩均收益: ¥${Math.round(report.summary.estimatedRevenue / report.summary.totalArea)}

+
+
+
+ +
+

风险分布

+
+
+

低风险地块

+

${report.summary.riskDistribution.low} 个

+
+
+
+
+
+

中等风险地块

+

${report.summary.riskDistribution.medium} 个

+
+
+
+
+
+

高风险地块

+

${report.summary.riskDistribution.high} 个

+
+
+
+
+
+
+ +
+

推荐作物排名

+ + + + + + + + + + + + + ${report.summary.topRecommendedCrops.map((crop, index) => ` + + + + + + + + + `).join('')} + +
排名作物名称类别推荐频次平均适宜性经济效益
${index + 1}${crop.crop}${crop.category}${crop.frequency} 次${crop.avgScore} 分¥${crop.economicValue}/亩
+
+ +
+

详细地块分析

+ ${report.results.map(result => { + const block = report.landBlocks.find(b => b.id === result.blockId); + return ` +
+

${result.blockName}

+

地块信息: ${block?.area} 亩 | ${block?.soilType} | ${block?.irrigation}

+

综合得分: ${result.overallScore} 分

+

推荐作物: ${result.recommendedCrops.slice(0, 3).map(c => c.crop.name).join('、')}

+

风险因素: ${result.riskFactors.join(';') || '无'}

+

改进建议: ${result.improvementSuggestions.join(';')}

+
+ `; + }).join('')} +
+ +
+

分析说明

+
+

评价方法

+

本次分析采用多因子综合评价法,考虑土壤条件、气候条件、地形条件和基础设施四个维度。

+

权重配置:

+
    +
  • 土壤条件: ${Math.round(report.weightConfig.soil * 100)}%
  • +
  • 气候条件: ${Math.round(report.weightConfig.climate * 100)}%
  • +
  • 地形条件: ${Math.round(report.weightConfig.topography * 100)}%
  • +
  • 基础设施: ${Math.round(report.weightConfig.infrastructure * 100)}%
  • +
+
+
+ +
+

本报告由智慧农业生产管理系统自动生成

+

生成时间: ${new Date().toLocaleString('zh-CN')}

+
+ + + `; + + // 下载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 ( + + +

暂无分析数据

+

+ 请先运行分析生成结果 +

+ +
+ ); + } + + return ( +
+ {/* 报告操作 */} +
+
+ +

分析报告

+
+
+ + +
+
+ + {/* 报告概览 */} + +
+ +

{currentReport.title}

+ + {currentReport.createdAt} + +
+ +
+
+
+ {currentReport.summary.totalBlocks} +
+
分析地块
+
+ +
+
+ {currentReport.summary.averageScore} +
+
平均得分
+
+ +
+
+ {currentReport.summary.totalArea} +
+
总面积(亩)
+
+ +
+
+ {Math.round(currentReport.summary.estimatedRevenue / 1000)}K +
+
预估收益
+
+
+
+ + {/* 推荐作物排名 */} + +

+ + 推荐作物排名 +

+ +
+ {currentReport.summary.topRecommendedCrops.map((crop, index) => ( +
+
+
+ {index + 1} +
+
+
{crop.crop}
+
{crop.category}
+
+
+ +
+
+
推荐频次
+
{crop.frequency} 次
+
+
+
适宜性
+
{crop.avgScore} 分
+
+
+
经济效益
+
¥{crop.economicValue}/亩
+
+
+
+ ))} +
+
+ + {/* 风险分析 */} + +

+ + 风险分析 +

+ +
+
+
+ 低风险地块 + {currentReport.summary.riskDistribution.low} 个 +
+ +

+ 综合得分≥80分,适宜种植推荐作物 +

+
+ +
+
+ 中等风险地块 + {currentReport.summary.riskDistribution.medium} 个 +
+ +

+ 综合得分60-79分,需要一定改进措施 +

+
+ +
+
+ 高风险地块 + {currentReport.summary.riskDistribution.high} 个 +
+ +

+ 综合得分<60分,需要重点改进 +

+
+
+
+ + {/* 详细分析结果 */} + +

+ + 详细分析结果 +

+ +
+ {currentReport.results.map(result => { + const block = currentReport.landBlocks.find(b => b.id === result.blockId); + return ( +
+
+
+ + {result.blockName} +
+ + {result.overallScore} 分 + +
+ +
+
+ 面积: + {block?.area} 亩 +
+
+ 土壤: + {block?.soilType} +
+
+ 灌溉: + {block?.irrigation} +
+
+ 预估收益: + ¥{result.economicReturn?.toLocaleString()} +
+
+ +
+
+ 推荐作物: +
+ {result.recommendedCrops.slice(0, 3).map((rec, index) => ( + + {rec.crop.name} ({rec.suitabilityScore}分) + + ))} +
+
+ +
+ 风险因素: +
+ {result.riskFactors.length > 0 ? result.riskFactors.join(';') : '无'} +
+
+
+
+ ); + })} +
+
+ + {/* 报告说明 */} + +
+ +
+

报告说明:

+
    +
  • • 本报告基于多因子综合评价模型,结合土壤、气候、地形、基础设施等条件进行分析
  • +
  • • 推荐作物基于各地区的农业实践数据和科学研究成果
  • +
  • • 经济效益估算仅供参考,实际收益受市场价格、管理水平等多种因素影响
  • +
  • • 建议结合实地调研和专家意见,制定最终的种植决策方案
  • +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisResults.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisResults.tsx new file mode 100644 index 0000000..693567c --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/AnalysisResults.tsx @@ -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; +} + +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 ( + +
+
+

+ + 地块适宜性评价结果 +

+

+ 共 {sortedResults.length} 个地块的评价结果 +

+
+
+ + + + + +
+
+ + {/* 统计摘要 */} +
+
+
+ {sortedResults.filter(r => r.overallScore >= 80).length} +
+
高度适宜
+
+
+
+ {sortedResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length} +
+
一般适宜
+
+
+
+ {sortedResults.filter(r => r.overallScore < 60).length} +
+
不适宜
+
+
+
+ {Math.round( + sortedResults.reduce((sum, r) => sum + r.overallScore, 0) / + sortedResults.length + )} +
+
平均得分
+
+
+ + {/* 结果表格 */} +
+ + + + 地块名称 + 综合得分 + 适宜性等级 + 土壤得分 + 气候得分 + 地形得分 + 基础设施得分 + 推荐作物 + 预估产量 + 经济效益 + 风险因素 + 改进建议 + + + + {sortedResults.map((result, index) => { + const topCrop = result.recommendedCrops[0]; + 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}
+
+ {topCrop.crop.category} +
+
+ )} +
+ +
+
{result.estimatedYield?.toLocaleString()} kg
+
+
+ +
+
¥{result.economicReturn?.toLocaleString()}
+
+
+ +
+ {result.riskFactors.length > 0 ? ( +
+ {result.riskFactors.join('、')} +
+ ) : ( +
无明显风险
+ )} +
+
+ +
+
+ {result.improvementSuggestions.join('、')} +
+
+
+
+ ); + })} +
+
+
+ + {/* 详细统计信息 */} +
+
+ +
+

统计分析

+
+
+
得分分布
+
    +
  • • 高度适宜 (80-100分): {sortedResults.filter(r => r.overallScore >= 80).length} 个地块
  • +
  • • 一般适宜 (60-79分): {sortedResults.filter(r => r.overallScore >= 60 && r.overallScore < 80).length} 个地块
  • +
  • • 不适宜 (0-59分): {sortedResults.filter(r => r.overallScore < 60).length} 个地块
  • +
+
+
+
经济效益
+
    +
  • • 总预估产量: {sortedResults.reduce((sum, r) => sum + (r.estimatedYield || 0), 0).toLocaleString()} kg
  • +
  • • 总经济效益: ¥{sortedResults.reduce((sum, r) => sum + (r.economicReturn || 0), 0).toLocaleString()}
  • +
  • • 平均亩产: {Math.round(sortedResults.reduce((sum, r) => sum + (r.estimatedYield || 0), 0) / sortedResults.length).toLocaleString()} kg
  • +
+
+
+
因子表现
+
    +
  • • 平均土壤得分: {Math.round(sortedResults.reduce((sum, r) => sum + r.soilScore, 0) / sortedResults.length)} 分
  • +
  • • 平均气候得分: {Math.round(sortedResults.reduce((sum, r) => sum + r.climateScore, 0) / sortedResults.length)} 分
  • +
  • • 平均地形得分: {Math.round(sortedResults.reduce((sum, r) => sum + r.topographyScore, 0) / sortedResults.length)} 分
  • +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/BatchAnalysisPanel.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/BatchAnalysisPanel.tsx new file mode 100644 index 0000000..2870a99 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/BatchAnalysisPanel.tsx @@ -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; +} + +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 ; + case 'running': + return ; + case 'paused': + return ; + case 'completed': + return ; + case 'failed': + return ; + default: + return ; + } + }; + + 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 ( +
+ {/* 批量分析控制面板 */} + +
+

+ + 批量评价地块 +

+ + 已选择 {state.selectedBlocks.length} / {state.landBlocks.length} 个地块 + +
+ + {/* 任务状态显示 */} + {currentTask && ( +
+
+
+ {getStatusIcon(currentTask.status)} +

{currentTask.name}

+
+ + {getStatusText(currentTask.status)} + +
+ +
+
+ 创建时间: +
{currentTask.createdAt}
+
+
+ 总地块: +
{currentTask.totalBlocks} 个
+
+
+ 已处理: +
{currentTask.processedBlocks} 个
+
+
+ 成功率: +
+ {currentTask.totalBlocks > 0 + ? Math.round((currentTask.successCount / currentTask.processedBlocks) * 100) + : 0}% +
+
+
+ + {/* 进度条 */} +
+
+ 分析进度 + {currentTask.progress}% +
+ +
+ + {/* 当前处理的地块 */} + {currentTask.currentProcessingBlock && ( +
+ 正在处理: + {currentTask.currentProcessingBlock} +
+ )} + + {/* 错误信息 */} + {currentTask.errorMessage && ( +
+ 错误: {currentTask.errorMessage} +
+ )} +
+ )} + + {/* 控制按钮 */} +
+ {!state.isBatchAnalyzing && !currentTask && ( + + )} + + {state.isBatchAnalyzing && currentTask && ( + <> + + + + )} + + {currentTask && currentTask.status === 'paused' && ( + <> + + + + )} + + {currentTask && currentTask.status === 'completed' && ( + + )} +
+
+ + {/* 分析配置信息 */} + +

+ + 分析配置 +

+ +
+
+
权重配置
+
+
+ 土壤条件: + {Math.round(state.weightConfig.soil * 100)}% +
+
+ 气候条件: + {Math.round(state.weightConfig.climate * 100)}% +
+
+ 地形条件: + {Math.round(state.weightConfig.topography * 100)}% +
+
+ 基础设施: + {Math.round(state.weightConfig.infrastructure * 100)}% +
+
+
+ +
+
评价因子
+
+
+ 启用因子: + {state.factors.filter(f => f.enabled).length} / {state.factors.length} +
+
+ 土壤因子: + {state.factors.filter(f => f.enabled && f.category === 'soil').length} 个 +
+
+ 气候因子: + {state.factors.filter(f => f.enabled && f.category === 'climate').length} 个 +
+
+ 地形因子: + {state.factors.filter(f => f.enabled && f.category === 'topography').length} 个 +
+
+
+
+
+ + {/* 地块分析状态概览 */} + {state.landBlocks.some(block => block.analysisStatus) && ( + +

+ + 地块分析状态 +

+ +
+
+
+ {state.landBlocks.filter(b => b.analysisStatus === 'pending').length} +
+
等待分析
+
+
+
+ {state.landBlocks.filter(b => b.analysisStatus === 'analyzing').length} +
+
正在分析
+
+
+
+ {state.landBlocks.filter(b => b.analysisStatus === 'completed').length} +
+
分析完成
+
+
+
+ {state.landBlocks.filter(b => b.analysisStatus === 'failed').length} +
+
分析失败
+
+
+
+ )} + + {/* 使用说明 */} + +
+ +
+

批量评价说明:

+
    +
  • 批量处理: 选择多个地块,系统将自动循环调用空间分析服务
  • +
  • 因子读取: 自动读取土壤、气候、地形、基础设施等各项因子数据
  • +
  • 加权计算: 根据配置的权重和因子进行综合评分计算
  • +
  • 适宜性指数: 生成0-100的适宜性指数并自动更新到数据库
  • +
  • 进度监控: 实时显示分析进度,支持暂停、继续、取消操作
  • +
  • 结果保存: 分析完成后自动保存结果,可查看详细报告
  • +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/BatchEvaluationPanel.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/BatchEvaluationPanel.tsx new file mode 100644 index 0000000..279a213 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/BatchEvaluationPanel.tsx @@ -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; +} + +export default function BatchEvaluationPanel({ state, dispatch }: BatchEvaluationPanelProps) { + const [batchManager, setBatchManager] = useState(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 ( +
+ {/* 主要批量分析控制面板 */} + +
+

+ + 批量评价地块 +

+
+ + 68 个地块 + + {currentTask && ( + + {currentTask.status === 'pending' && '等待中'} + {currentTask.status === 'running' && '分析中'} + {currentTask.status === 'completed' && '已完成'} + {currentTask.status === 'failed' && '失败'} + {currentTask.status === 'paused' && '已暂停'} + {currentTask.status === 'cancelled' && '已取消'} + + )} +
+
+ + {/* 控制按钮区域 */} +
+ {!currentTask || currentTask.status === 'completed' || currentTask.status === 'failed' || currentTask.status === 'cancelled' ? ( + + ) : ( + <> + {currentTask.status === 'running' && ( + + )} + {currentTask.status === 'paused' && ( + + )} + + + )} +
+ + {/* 权重和因子验证警告 */} + {totalWeight !== 100 && ( +
+

+ ⚠️ 权重总和必须为100%才能进行批量分析(当前:{totalWeight}%) +

+
+ )} + + {state.factors.filter(f => f.enabled).length < 3 && ( +
+

+ ⚠️ 至少需要启用3个评价因子才能进行批量分析(当前:{state.factors.filter(f => f.enabled).length}个) +

+
+ )} + + {/* 分析进行中的状态显示 */} + {currentTask && (currentTask.status === 'running' || currentTask.status === 'paused') && ( +
+
+ 分析进度 + {currentTask.progress}% +
+ + +
+
+

当前处理

+

{currentTask.currentProcessingBlock || '准备中...'}

+
+
+

处理进度

+

+ {currentTask.processedBlocks} / {currentTask.totalBlocks} 地块 +

+
+
+

成功数量

+

{currentTask.successCount}

+
+
+

失败数量

+

{currentTask.failedCount}

+
+
+ +
+

✓ 正在从空间分析服务读取因子数据...

+

✓ 正在计算各因子得分(pH、有机质、土层厚度、全氮、全磷、全钾、排水性)...

+

✓ 正在进行加权汇总计算...

+

✓ 正在生成适宜性指数并更新到数据库...

+
+ + {/* 任务状态信息 */} +
+
+
+ 任务名称: + {currentTask.name} +
+
+ 创建时间: + {currentTask.createdAt} +
+ {currentTask.startedAt && ( +
+ 开始时间: + {currentTask.startedAt} +
+ )} +
+ 启用因子: + {state.factors.filter(f => f.enabled).length}个 +
+
+
+
+ )} + + {/* 分析完成状态 */} + {currentTask && currentTask.status === 'completed' && ( +
+

+ ✓ 批量分析已完成!已为 {currentTask.totalBlocks} 个地块生成适宜性评价结果并更新到数据库 +

+
+
完成时间:{currentTask.completedAt}
+
成功分析:{currentTask.successCount} 个地块
+ {currentTask.failedCount > 0 && ( +
分析失败:{currentTask.failedCount} 个地块
+ )} +
+
+ )} + + {/* 分析失败状态 */} + {currentTask && currentTask.status === 'failed' && ( +
+

+ ✗ 批量分析失败:{currentTask.errorMessage || '未知错误'} +

+
+
已处理:{currentTask.processedBlocks} 个地块
+
成功:{currentTask.successCount} 个地块
+
+
+ )} +
+ + {/* 批量分析结果统计 */} + {resultsForStats.length > 0 && ( +
+ +
+
+

高度适宜

+

{highSuitability}

+

+ 地块数量 ({Math.round((highSuitability / resultsForStats.length) * 100)}%) +

+
+ +
+
+ + +
+
+

一般适宜

+

{mediumSuitability}

+

+ 地块数量 ({Math.round((mediumSuitability / resultsForStats.length) * 100)}%) +

+
+ +
+
+ + +
+
+

不适宜

+

{lowSuitability}

+

+ 地块数量 ({Math.round((lowSuitability / resultsForStats.length) * 100)}%) +

+
+ +
+
+
+ )} + + {/* 分析说明卡片 */} + +

+ + 空间分析服务说明 +

+ +
+
+
+
+ +
+
土壤因子
+

+ pH值、有机质、土层厚度、全氮、全磷、全钾、排水性 +

+
+ +
+
+ +
+
水分因子
+

+ 地下水位、灌溉保证率、排水能力、土壤含水量 +

+
+ +
+
+ +
+
气候因子
+

+ 年均温度、年降雨量、日照时数、无霜期、积温 +

+
+ +
+
+ +
+
地形因子
+

+ 海拔高度、坡度、坡向、地貌类型、侵蚀程度 +

+
+
+ +
+
+ +
+

批量评价流程:

+
    +
  • 数据读取: 自动从空间分析服务读取68个地块的多维度因子数据
  • +
  • 因子评分: 根据最优值范围计算每个因子的得分(0-100分)
  • +
  • 加权汇总: 按照配置的权重计算综合适宜性指数
  • +
  • 等级划分: 根据综合得分确定适宜性等级(高度适宜/一般适宜/不适宜)
  • +
  • 数据库更新: 将评价结果和适宜性指数自动更新到数据库
  • +
  • 结果统计: 生成详细的统计分析和可视化报告
  • +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/EvaluationModel.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/EvaluationModel.tsx new file mode 100644 index 0000000..fb38abf --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/EvaluationModel.tsx @@ -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; +} + +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: , + climate: , + topography: , + infrastructure: + }; + return icons[category as keyof typeof icons] || ; + }; + + 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: , + factors: state.factors.filter(f => f.category === 'soil') + }, + { + key: 'climate', + name: '气候因子', + icon: , + factors: state.factors.filter(f => f.category === 'climate') + }, + { + key: 'topography', + name: '地形因子', + icon: , + factors: state.factors.filter(f => f.category === 'topography') + }, + { + key: 'infrastructure', + name: '基础设施因子', + icon: , + factors: state.factors.filter(f => f.category === 'infrastructure') + } + ]; + + return ( + +
+
+
+ +
+
+

评价模型配置

+

选择和配置评价因子

+
+
+
+ + {enabledFactorCount}/{totalFactorCount} 个因子 + + +
+
+ + {/* 评价因子配置区域 */} +
+ {factorCategories.map((category) => ( +
+
+ {category.icon} +

{category.name}

+ + {category.factors.filter(f => f.enabled).length}/{category.factors.length} + +
+ +
+ {category.factors.map((factor) => ( +
+
+
+
+ {getFactorIcon(factor.category)} +
{factor.name}
+ {factor.enabled ? ( + + ) : ( + + )} +
+

+ {factor.description} +

+
+ + 权重: {Math.round(factor.weight * 100)}% + + handleToggleFactor(factor.id)} + disabled={state.isBatchAnalyzing} + /> +
+
+
+
+ ))} +
+
+ ))} +
+ + {/* 模型配置说明 */} +
+
+ +
+

+ 评价模型说明 +

+
+

+ 多因子加权评价模型采用层次分析法(AHP)和专家评分法, + 结合农业生产实际需求,构建科学的评价指标体系。 +

+
+
+
评价因子选择原则:
+
    +
  • 主导性:选择对作物生长起决定性作用的因子
  • +
  • 独立性:避免因子间的重复和高度相关性
  • +
  • 可获取性:确保数据能够准确获取和更新
  • +
  • 稳定性:优先选择相对稳定的评价因子
  • +
+
+
+
权重确定方法:
+
    +
  • 专家打分:邀请农业专家进行权重评分
  • +
  • 层次分析:构建判断矩阵确定权重
  • +
  • 实地验证:结合实际产量数据校准权重
  • +
  • 动态调整:根据作物类型和生产目标调整
  • +
+
+
+
+
+
+
+ + {/* 启用因子过少警告 */} + {enabledFactorCount < 6 && ( +
+
+ +
+

+ 评价因子配置建议 +

+

+ 当前仅启用了 {enabledFactorCount} 个评价因子,建议至少启用 6-8 个因子以获得更准确的评价结果。 + 确保各类别都有代表性因子参与评价。 +

+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/FactorConfiguration.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/FactorConfiguration.tsx new file mode 100644 index 0000000..8c4f40b --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/FactorConfiguration.tsx @@ -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; + 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 = ( +
+ {/* 权重总览 */} + +
+

权重分配总览

+
+ 总权重: + + {Math.round(getTotalWeight() * 100)}% + + +
+
+ +
+ {categories.map(category => { + const totalWeight = getCategoryTotal(category.id); + return ( +
+
+ {category.name} + + {Math.round(totalWeight * 100)}% + +
+ +
+ ); + })} +
+
+ + {/* 因子详细配置 */} +
+ {categories.map(category => ( + +

+
+ {category.name} + + {state.factors.filter(f => f.category === category.id).length} 个因子 + +

+ +
+ {state.factors + .filter(factor => factor.category === category.id) + .map(factor => ( +
+
+
+
+

{factor.name}

+ {factor.enabled ? ( + + ) : ( + + )} +
+

+ {factor.description} +

+
+ +
+
+
+ {Math.round(factor.weight * 100)}% +
+
权重
+
+ + handleToggleFactor(factor.id)} + /> +
+
+ + {/* 权重调节 */} + {factor.enabled && ( +
+
+ 权重调节 + {Math.round(factor.weight * 100)}% +
+ + handleUpdateWeight(factor.id, value / 100) + } + max={50} + min={0} + step={1} + className="w-full" + /> +
+ )} +
+ ))} +
+
+ ))} +
+ + {/* 配置说明 */} + +
+ +
+

配置说明:

+
    +
  • • 权重总和应保持100%,系统会自动计算各因子的影响程度
  • +
  • • 启用/禁用因子来控制评价维度,禁用的因子不参与计算
  • +
  • • 土壤条件、气候条件、地形条件、基础设施为四大评价维度
  • +
  • • 可以根据具体需求调整各因子的权重,建议咨询农业专家
  • +
  • • 点击"重置默认"可以恢复到推荐的标准权重配置
  • +
+
+
+
+
+ ); + + if (isDialog) { + return ( + dispatch({ type: 'TOGGLE_FACTOR_DIALOG' })}> + + + + + 评价因子配置 + + + {content} + + + ); + } + + return ( +
+
+

评价因子配置

+
+ + 启用 {state.factors.filter(f => f.enabled).length} / {state.factors.length} + + +
+
+ + {content} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/FactorWeights.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/FactorWeights.tsx new file mode 100644 index 0000000..767fd51 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/FactorWeights.tsx @@ -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; +} + +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: , + climate: , + topography: , + infrastructure: + }; + return icons[category as keyof typeof icons] || ; + }; + + const factorCategories = [ + { + key: 'soil' as const, + name: '土壤因子', + icon: , + description: 'pH值、有机质、全氮、全磷、全钾、土壤质地等', + factors: state.factors.filter(f => f.category === 'soil') + }, + { + key: 'climate' as const, + name: '气候因子', + icon: , + description: '年均温度、年降雨量、日照时数、无霜期等', + factors: state.factors.filter(f => f.category === 'climate') + }, + { + key: 'topography' as const, + name: '地形因子', + icon: , + description: '海拔高度、坡度、坡向、地貌类型等', + factors: state.factors.filter(f => f.category === 'topography') + }, + { + key: 'infrastructure' as const, + name: '基础设施因子', + icon: , + description: '灌溉条件、交通便利性、排水条件等', + factors: state.factors.filter(f => f.category === 'infrastructure') + } + ]; + + return ( + +
+
+
+ +
+
+

因子权重配置

+

调整各类因子的权重占比

+
+
+
+ + 总权重: {totalWeight}% + + +
+
+ + {/* 权重配置区域 */} +
+ {factorCategories.map((category) => ( +
+
+
+ {getWeightIcon(category.key)} +
+

{category.name}

+

+ {category.description} +

+
+
+
+
+ {Math.round(state.weightConfig[category.key] * 100)}% +
+
+ {category.factors.length} 个因子 +
+
+
+ +
+
+ 权重调整 + {Math.round(state.weightConfig[category.key] * 100)}% +
+ handleWeightChange(category.key, value)} + max={60} + min={5} + step={5} + className="w-full" + disabled={state.isBatchAnalyzing} + /> + +
+ + {/* 显示该类别下的主要因子 */} +
+
主要评价因子:
+
+ {category.factors.slice(0, 3).map((factor) => ( + + {factor.name} + + ))} + {category.factors.length > 3 && ( + + +{category.factors.length - 3} + + )} +
+
+
+ ))} +
+ + {/* 权重总和警告 */} + {totalWeight !== 100 && ( +
+
+ +
+

+ 权重总和验证 +

+

+ 所有因子权重总和必须为100%才能进行批量分析。 + 当前总和为 {totalWeight}%,{totalWeight > 100 ? '超出' : '还差'} {Math.abs(totalWeight - 100)}%。 +

+

+ 💡 建议权重分配:土壤因子(35%)、气候因子(30%)、地形因子(20%)、基础设施因子(15%) +

+
+
+
+ )} + + {/* 权重说明 */} +
+
+ +
+

+ 权重配置说明 +

+
    +
  • 土壤因子:反映地块基础肥力和物理化学性质
  • +
  • 气候因子:决定光热水的匹配度和作物适应性
  • +
  • 地形因子:影响水土保持和机械化作业条件
  • +
  • 基础设施因子:体现生产条件和集约化水平
  • +
+

+ 权重配置将直接影响最终适宜性指数的计算结果,请根据实际生产需求科学分配。 +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/LandBlockSelector.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/LandBlockSelector.tsx new file mode 100644 index 0000000..76adac1 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/LandBlockSelector.tsx @@ -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; + 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 ( +
+
+

选择分析地块

+ +
+ +
+ {state.landBlocks.map(block => { + const isSelected = state.selectedBlocks.includes(block.id); + const score = getBlockScore(block.id); + + return ( +
dispatch({ type: 'TOGGLE_BLOCK_SELECTION', payload: block.id })} + > +
+
+ +
+

{block.name}

+

+ {block.area} 亩 · {block.soilType} · {block.irrigation} +

+
+
+ {score && ( + + {score}分 + + )} +
+
+ ); + })} +
+
+ ); + } + + return ( + +
+

地块选择

+
+ + + 已选择 {state.selectedBlocks.length} / {state.landBlocks.length} + +
+
+ + {/* 地块列表 */} +
+ {state.landBlocks.length === 0 ? ( +
+ +

暂无地块数据

+

请先添加地块信息

+
+ ) : ( + state.landBlocks.map(block => { + const isSelected = state.selectedBlocks.includes(block.id); + const score = getBlockScore(block.id); + + return ( +
dispatch({ type: 'TOGGLE_BLOCK_SELECTION', payload: block.id })} + > +
+
+
+ {isSelected ? ( + + ) : ( + + )} +
+ +
+
+

{block.name}

+ {block.currentCrop && ( + + {block.currentCrop} + + )} +
+ + {/* 地块基本信息 */} +
+
+ 面积: + {block.area} 亩 +
+
+ 土壤: + {block.soilType} +
+
+ 灌溉: + {block.irrigation} +
+
+ 海拔: + {block.elevation}m +
+
+ + {/* 土壤指标 */} +
+
+ pH: + {block.pH} +
+
+ 有机质: + {block.organicMatter}% +
+
+ 氮: + {block.nitrogen} +
+
+ 磷: + {block.phosphorus} +
+
+ 钾: + {block.potassium} +
+
+
+
+ + {/* 分析状态和适宜性指数 */} + {(score || block.analysisStatus) && ( +
+ {block.analysisStatus === 'analyzing' && ( +
+ +
分析中
+
+ )} + {block.analysisStatus === 'failed' && ( +
+ +
分析失败
+
+ )} + {block.analysisStatus === 'completed' && block.suitabilityIndex && ( +
+
+ {block.suitabilityIndex} +
+
+ 适宜性指数 +
+ +
+ {block.lastAnalyzed} +
+
+ )} + {score && !block.analysisStatus && ( +
+
+ {score} +
+
+ {getScoreLabel(score)} +
+ +
+ )} +
+ )} +
+
+ ); + }) + )} +
+ + {/* 选中地块统计 */} + {state.selectedBlocks.length > 0 && ( +
+

选中地块统计

+
+
+ 地块数量: + {state.selectedBlocks.length} 个 +
+
+ 总面积: + + {state.landBlocks + .filter(b => state.selectedBlocks.includes(b.id)) + .reduce((sum, b) => sum + b.area, 0) + .toFixed(1)} 亩 + +
+
+ 平均得分: + + {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 + ) + : '--' + } 分 + +
+
+ 土壤类型: + + {[...new Set( + state.landBlocks + .filter(b => state.selectedBlocks.includes(b.id)) + .map(b => b.soilType) + )].join(', ')} + +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/SpatialAnalysisContent.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/SpatialAnalysisContent.tsx new file mode 100644 index 0000000..38cd4e6 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/SpatialAnalysisContent.tsx @@ -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; +} + +export default function SpatialAnalysisContent({ state, dispatch }: SpatialAnalysisContentProps) { + return ( +
+ {/* 标题区域 */} +
+

自动化空间分析

+

+ 多因子评价模型,批量生成地块适宜性指数并更新到数据库 +

+
+ + {/* 权重配置面板 */} + + + {/* 评价模型配置 */} + + + {/* 批量评价面板 */} + + + {/* 分析结果记录 */} + {state.analysisResults.length > 0 && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/SpatialDistribution.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/SpatialDistribution.tsx new file mode 100644 index 0000000..ebf035a --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/SpatialDistribution.tsx @@ -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; +} + +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 ( + +
+

+ + 空间分布可视化 +

+ + {selectedBlocksWithScores.length} 个地块 + +
+ + {/* 简化的地图展示 */} +
+ {selectedBlocksWithScores.length === 0 ? ( +
+ +

请先选择地块以查看空间分布

+
+ ) : ( +
+ {/* 模拟地图背景 */} +
+ + {/* 地块分布 */} +
+
+ {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 ( +
+ {/* 地块标记 */} +
+
+
+ +
+ + {/* 地块信息卡片 */} +
+
+

{item.block!.name}

+ + {item.result?.overallScore || '--'}分 + +
+ +
+
+ 面积: + {item.block!.area} 亩 +
+
+ 土壤: + {item.block!.soilType} +
+ {item.result && ( + <> +
+ 土壤得分: + {Math.round(item.result.soilScore)} +
+
+ 气候得分: + {Math.round(item.result.climateScore)} +
+ + )} + {item.result?.recommendedCrops[0] && ( +
+ 推荐: {item.result.recommendedCrops[0].crop.name} +
+ )} +
+
+
+
+ ); + })} +
+
+ + {/* 图例 */} +
+

+ + 适宜性等级 +

+
+
+
+ 优秀 (≥80分) +
+
+
+ 良好 (60-79分) +
+
+
+ 一般 (40-59分) +
+
+
+ 较差 (<40分) +
+
+
+ + {/* 指北针 */} +
+
+ +
+
+
+ )} +
+ + {/* 统计信息 */} + {selectedBlocksWithScores.length > 0 && ( +
+
+
+ {selectedBlocksWithScores.filter(item => (item.result?.overallScore || 0) >= 80).length} +
+
优秀地块
+
+
+
+ {Math.round( + selectedBlocksWithScores.reduce((sum, item) => sum + (item.block?.area || 0), 0) + )} +
+
总面积(亩)
+
+
+
+ {selectedBlocksWithScores.length > 0 + ? Math.round( + selectedBlocksWithScores.reduce((sum, item) => sum + (item.result?.overallScore || 0), 0) / + selectedBlocksWithScores.length + ) + : 0} +
+
平均得分
+
+
+
+ {[...new Set( + selectedBlocksWithScores + .filter(item => item.result?.recommendedCrops[0]) + .map(item => item.result!.recommendedCrops[0].crop.name) + )].length} +
+
推荐作物
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/WeightConfiguration.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/WeightConfiguration.tsx new file mode 100644 index 0000000..899efa1 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/WeightConfiguration.tsx @@ -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; + 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 = ( +
+ {/* 权重总览 */} + +
+

+ + 权重分配总览 +

+
+
+
+ {Math.round(getTotalWeight() * 100)}% +
+
总权重
+
+ + +
+
+ + {/* 权重可视化 */} +
+ {categoryConfigs.map(category => ( +
+
+
+ {category.icon} + {category.name} +
+
+ {category.percentage}% + + {category.weight.toFixed(2)} + +
+
+ + +
+ ))} + + {Math.abs(getTotalWeight() - 1) > 0.01 && ( +
+
+ 偏差 + + {Math.abs((getTotalWeight() - 1) * 100).toFixed(1)}% + +
+ +

+ 权重总和不为100%,建议点击"自动平衡"调整 +

+
+ )} +
+
+ + {/* 权重调节面板 */} + +

权重调节

+ +
+ {categoryConfigs.map(category => ( +
+
+
+ {category.icon} +
+

{category.name}

+

+ {category.id === 'soil' && '包括土壤pH、有机质、养分含量等指标'} + {category.id === 'climate' && '包括温度、降水、光照等气象条件'} + {category.id === 'topography' && '包括海拔、坡度等地形特征'} + {category.id === 'infrastructure' && '包括灌溉、交通等设施条件'} +

+
+
+ +
+
{category.percentage}%
+
当前权重
+
+
+ +
+ handleUpdateWeight(category.id, value)} + max={60} + min={5} + step={1} + className="w-full" + /> +
+ 5% + 60% +
+
+
+ ))} +
+
+ + {/* 权重配置说明 */} + +
+ +
+

权重配置说明:

+
    +
  • 土壤条件 (35%): 土壤是农业生产的基础,影响作物生长和养分供应
  • +
  • 气候条件 (30%): 气候决定了作物的生长环境和适宜种植范围
  • +
  • 地形条件 (20%): 地形影响水土保持、机械化作业和微气候
  • +
  • 基础设施 (15%): 灌溉、交通等设施影响生产效率和成本
  • +
  • • 权重总和应保持100%,可根据实际情况调整各维度的重要性
  • +
  • • 建议在农业专家指导下进行权重配置,确保评价结果科学合理
  • +
+
+
+
+ + {/* 预设配置 */} + +

快速预设配置

+
+ + + + + +
+
+
+ ); + + if (isDialog) { + return ( + dispatch({ type: 'TOGGLE_WEIGHT_DIALOG' })}> + + + + + 权重配置 + + + {content} + + + ); + } + + return ( +
+
+

权重配置

+
+ + 总权重: {Math.round(getTotalWeight() * 100)}% + +
+
+ + {content} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/batchAnalysisService.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/batchAnalysisService.tsx new file mode 100644 index 0000000..a5cad72 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/batchAnalysisService.tsx @@ -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 { + // 模拟从各种数据源读取因子数据 + 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; + private isRunning: boolean = false; + private currentTask: BatchAnalysisTask | null = null; + + constructor(dispatch: React.Dispatch) { + this.service = new MockSpatialAnalysisService(); + this.dispatch = dispatch; + } + + async startBatchAnalysis( + taskName: string, + factors: EvaluationFactor[], + weightConfig: any + ): Promise { + 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 { + // 等待任务创建完成 - 直接返回一个虚拟任务 + 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; \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/components/spatialAnalysisReducer.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/components/spatialAnalysisReducer.tsx new file mode 100644 index 0000000..60b6498 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/components/spatialAnalysisReducer.tsx @@ -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 } } + | { 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 } + | { type: 'RESET_FILTERS' } + + | { type: 'SET_CHART_DATA'; payload: Partial } + + | { 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 } } + | { 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; + } +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/auto/page.tsx b/crop-x/src/app/(app)/land-information/suitability/auto/page.tsx new file mode 100644 index 0000000..ef3002d --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/auto/page.tsx @@ -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 ( +
+ +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/batch/page.tsx b/crop-x/src/app/(app)/land-information/suitability/batch/page.tsx deleted file mode 100644 index b3e2a4f..0000000 --- a/crop-x/src/app/(app)/land-information/suitability/batch/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Card } from '@/components/ui/card'; - -export default function BatchPage() { - return ( -
- -

批量适宜性评价

-
-

- 页面路径: /land-information/suitability/batch -

-
-
-
- ); -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/comprehensive/page.tsx b/crop-x/src/app/(app)/land-information/suitability/comprehensive/page.tsx deleted file mode 100644 index 63e3c5f..0000000 --- a/crop-x/src/app/(app)/land-information/suitability/comprehensive/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Card } from '@/components/ui/card'; - -export default function ComprehensivePage() { - return ( -
- -

综合适宜性评价

-
-

- 页面路径: /land-information/suitability/comprehensive -

-
-
-
- ); -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/crop/page.tsx b/crop-x/src/app/(app)/land-information/suitability/crop/page.tsx deleted file mode 100644 index 5db96d3..0000000 --- a/crop-x/src/app/(app)/land-information/suitability/crop/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Card } from '@/components/ui/card'; - -export default function CropPage() { - return ( -
- -

作物适宜性评价

-
-

- 页面路径: /land-information/suitability/crop -

-
-
-
- ); -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx new file mode 100644 index 0000000..065728a --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx @@ -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({ + total: 0, + processed: 0, + highSuitability: 0, + mediumSuitability: 0, + lowSuitability: 0, + currentField: '', + }); + const [isBatchRunning, setIsBatchRunning] = useState(false); + const [batchAnalysisResults, setBatchAnalysisResults] = useState([]); + + // 评价因子权重配置 + const [factorWeights, setFactorWeights] = useState([ + { 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( + 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 ( +
+
+
+

多因子综合评价

+

+ 地块适宜性评价、自动化空间分析与作物适配推荐 +

+
+
+ + +
+
+ + {/* 多因子综合评价 */} +
+ +
+
+ + +
+ +
+ +
+
+
+ + {/* 评价结果总览 */} +
+ +
+ +

综合评分

+

+ {currentResult.totalScore} +

+ + {currentResult.grade} + +
+
+ + +
+ +

优秀因子

+

+ {currentResult.factors.filter(f => f.score >= 80).length} +

+

+ / {currentResult.factors.length} 项 +

+
+
+ + +
+ +

待改善因子

+

+ {currentResult.factors.filter(f => f.score < 70).length} +

+

需要优化

+
+
+ + +
+ +

评价时间

+

+ {formatDate(currentResult.timestamp)} +

+

最近更新

+
+
+
+ + {/* 因子详细评分 */} + +

评价因子详细分析

+
+ {currentResult.factors.map((factor) => ( +
+
+
+ {factor.name} + + 权重: {factor.weight}% + +
+
+ + 实际值: {factor.value.toFixed(1)}{factor.unit} + + + 最佳范围: {factor.optimalRange[0]}-{factor.optimalRange[1]}{factor.unit} + + + {factor.score}分 + +
+
+
+ +
+
+
+
+
+
+ ))} +
+ + + {/* 加权计算说明 */} + +

层次分析法(AHP)加权计算

+
+
+

计算公式:

+ + 总分 = Σ(因子得分 × 因子权重) + +

+ = ({currentResult.factors.map((f, i) => + `${f.score} × ${f.weight}%${i < currentResult.factors.length - 1 ? ' + ' : ''}` + ).join('')}) +

+

+ = {currentResult.totalScore}分 +

+
+ +
+
+

高度适宜 (≥80分)

+
    +
  • • 土壤条件优秀
  • +
  • • 适合多种作物
  • +
  • • 预期产量高
  • +
+
+
+

一般适宜 (60-79分)

+
    +
  • • 部分因子需改善
  • +
  • • 可种植耐性作物
  • +
  • • 需适当改良
  • +
+
+
+

不适宜 (<60分)

+
    +
  • • 土壤条件较差
  • +
  • • 需大幅改良
  • +
  • • 风险较高
  • +
+
+
+
+
+ + {/* 多因子综合评价功能说明 */} + +
+ +
+

多因子综合评价功能说明:

+
    +
  • 关键指标整合: 整合pH值、有机质含量、土层厚度、全氮、全磷、全钾、排水性等7项关键土壤指标
  • +
  • 层次分析法(AHP): 采用层次分析法构建加权评分体系,公式:总分 = Σ(因子得分 × 因子权重)
  • +
  • 单因子评分: 根据实际值与最佳范围的接近程度计算各因子得分(0-100分),范围内得分85-100分
  • +
  • 自定义权重: 支持手动调整各指标权重,提供粮食作物和经济作物两种预设方案
  • +
  • 综合得分: 输出0-100分的适宜性评分,自动计算加权总分
  • +
  • 三级分级: 高度适宜(≥80分)、一般适宜(60-79分)、不适宜(<60分)
  • +
  • 可视化展示: 提供进度条、颜色标识等多维度可视化,直观展示各因子评分与最佳范围对比
  • +
  • 改善建议: 自动识别待改善因子,为土壤改良提供决策依据
  • +
+
+
+
+
+ + {/* 权重配置对话框 */} + + + + 评价因子权重配置 + + 调整各因子的权重百分比以适应不同的评价需求 + + + +
+ {/* 预设方案 */} +
+

预设权重方案

+
+ + + + +
+
+ + {/* 权重调整 */} +
+
+

因子权重调整

+ + 总计: {totalWeight}% + +
+ +
+ {factorWeights.map((factor) => ( +
+
+ {factor.name} +
+ handleUpdateWeight(factor.id, parseInt(e.target.value) || 0)} + className="w-16 h-8 text-xs" + min={0} + max={100} + /> + % +
+
+ handleUpdateWeight(factor.id, value)} + max={100} + step={1} + className="h-2" + /> +
+ ))} +
+ + {totalWeight !== 100 && ( +
+

+ ⚠️ 权重总和必须为100%(当前:{totalWeight}%) +

+
+ )} +
+ + {/* 权重说明 */} +
+

权重配置说明

+
+

权重总和: 所有因子权重总和必须为100%

+

权重影响: 权重越高的因子对最终得分影响越大

+

预设方案: 系统提供针对不同作物类型的优化权重配置

+

自定义配置: 可根据具体需求手动调整各因子权重

+
+
+
+
+
+ + {/* 作物知识库对话框 */} + + + + 作物知识库 + + 查看系统中支持的作物品种及其生长条件要求 + + + +
+ {/* 知识库统计 */} +
+ +
+
+ {cropKnowledgeBase.length} +
+
作物品种
+
+
+ +
+
+ {new Set(cropKnowledgeBase.map(crop => crop.category)).size} +
+
作物类别
+
+
+ +
+
+ {cropKnowledgeBase.reduce((sum, crop) => sum + crop.riskFactors.length, 0)} +
+
风险因子
+
+
+
+ + {/* 作物列表 */} +
+ {cropKnowledgeBase.map((crop) => ( + +
+
+
+

{crop.cropName}

+ + {crop.category} + + + {crop.growthCycle.days}天 + +
+ +

+ {crop.description} +

+ +
+ {/* 土壤要求 */} +
+
+ + 土壤要求 +
+
+
+ pH值 + {crop.soilRequirements.ph.optimal[0]}-{crop.soilRequirements.ph.optimal[1]} +
+
+ 有机质 (g/kg) + {crop.soilRequirements.organicMatter.optimal[0]}-{crop.soilRequirements.organicMatter.optimal[1]} +
+
+ 土层厚度 (cm) + {crop.soilRequirements.soilDepth.optimal[0]}-{crop.soilRequirements.soilDepth.optimal[1]} +
+
+ 全氮 (g/kg) + {crop.soilRequirements.nitrogen.optimal[0]}-{crop.soilRequirements.nitrogen.optimal[1]} +
+
+ 全磷 (g/kg) + {crop.soilRequirements.phosphorus.optimal[0]}-{crop.soilRequirements.phosphorus.optimal[1]} +
+
+ 全钾 (g/kg) + {crop.soilRequirements.potassium.optimal[0]}-{crop.soilRequirements.potassium.optimal[1]} +
+
+
+ + {/* 气候要求 */} +
+
+ + 气候要求 +
+
+
+ 温度 (°C) + {crop.climateRequirements.temperature.optimal[0]}-{crop.climateRequirements.temperature.optimal[1]} +
+
+ 降雨 (mm/年) + {crop.climateRequirements.rainfall.optimal[0]}-{crop.climateRequirements.rainfall.optimal[1]} +
+
+ 光照 (小时/天) + {crop.climateRequirements.sunlight.optimal[0]}-{crop.climateRequirements.sunlight.optimal[1]} +
+
+ + {/* 预期产量 */} +
+
+ + 预期产量 (kg/亩) +
+
+
+
+ {crop.expectedYield.high[0]}-{crop.expectedYield.high[1]} +
+
高产
+
+
+
+ {crop.expectedYield.medium[0]}-{crop.expectedYield.medium[1]} +
+
中产
+
+
+
+ {crop.expectedYield.low[0]}-{crop.expectedYield.low[1]} +
+
低产
+
+
+
+
+
+ + {/* 风险因子 */} + {crop.riskFactors.length > 0 && ( +
+
+ + 主要风险因子 +
+
+ {crop.riskFactors.map((risk) => ( +
+
+ + {risk.name} +
+

{risk.suggestion}

+
+ ))} +
+
+ )} +
+
+
+ ))} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/cropKnowledgeBase.ts b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/cropKnowledgeBase.ts new file mode 100644 index 0000000..28efa0b --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/cropKnowledgeBase.ts @@ -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; +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/multiFactorService.ts b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/multiFactorService.ts new file mode 100644 index 0000000..e21d378 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/multiFactorService.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/multiFactorTypes.ts b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/multiFactorTypes.ts new file mode 100644 index 0000000..6386593 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/multiFactor/components/multiFactorTypes.ts @@ -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' + }); +}; \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/multiFactor/page.tsx b/crop-x/src/app/(app)/land-information/suitability/multiFactor/page.tsx new file mode 100644 index 0000000..93cef4b --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/multiFactor/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { MultiFactorEvaluation } from './components/MultiFactorEvaluation'; + +export default function ComprehensivePage() { + return ; +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx b/crop-x/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx new file mode 100644 index 0000000..695eae6 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx @@ -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 ( + +
+

+ + 智能作物推荐清单 +

+ + 基于{cropKnowledgeBase.length}种作物知识库匹配 + +
+ +
+ {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 ( + + {/* 标题栏 */} +
+
+
+ +
+
+
+

{crop.cropName}

+ {crop.category} +
+
+ + {suitabilityLevel} + + 匹配度: {matchScore}分 +
+
+
+
+

预期产量区间

+

+ {expectedYield[0]}-{expectedYield[1]} kg/亩 +

+

生长周期: {crop.growthCycle.days}天

+
+
+ + {/* 作物描述 */} +

+ {crop.description} +

+ + {/* 土壤因子匹配详情 */} +
+

土壤因子匹配情况:

+
+ {matchDetails.map((detail, i) => ( +
+

{detail.factor}

+

{detail.value.toFixed(1)}

+ {detail.status === '最佳' ? ( + + 最佳 + + ) : detail.status === '可接受' ? ( + + 可接受 + + ) : ( + + 偏离 + + )} +
+ ))} +
+
+ + {/* 气候要求 */} +
+
+

+ + 温度要求 +

+

+ {crop.climateRequirements.temperature.optimal[0]}-{crop.climateRequirements.temperature.optimal[1]}°C +

+
+
+

+ + 降雨要求 +

+

+ {crop.climateRequirements.rainfall.optimal[0]}-{crop.climateRequirements.rainfall.optimal[1]}mm/年 +

+
+
+

+ + 光照要求 +

+

+ {crop.climateRequirements.sunlight.optimal[0]}-{crop.climateRequirements.sunlight.optimal[1]}小时/天 +

+
+
+ + {/* 风险提示 */} + {applicableRisks.length > 0 && ( +
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' + }`}> +

+ r.severity === 'high') + ? 'text-red-600 dark:text-red-400' + : 'text-orange-600 dark:text-orange-400' + }`} /> + r.severity === 'high') ? 'text-red-900 dark:text-red-100' : 'text-orange-900 dark:text-orange-100'}> + 风险提示与应对建议 + +

+
+ {applicableRisks.map((risk, i) => ( +
+
+ + {risk.severity === 'high' ? '高风险' : risk.severity === 'medium' ? '中风险' : '低风险'} + +
+

r.severity === 'high') + ? 'text-red-800 dark:text-red-200' + : 'text-orange-800 dark:text-orange-200' + }`}> + {risk.name} +

+

触发条件: {risk.condition}

+

r.severity === 'high') ? 'text-red-700 dark:text-red-300' : 'text-orange-700 dark:text-orange-300'}> + 💡 {risk.suggestion} +

+
+
+
+ ))} +
+
+ )} + + {/* 适宜季节 */} +
+ 适宜种植季节: + {crop.growthCycle.seasons.map((season, i) => ( + + {season} + + ))} +
+
+ ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/recommend/components/FieldEnvironmentOverview.tsx b/crop-x/src/app/(app)/land-information/suitability/recommend/components/FieldEnvironmentOverview.tsx new file mode 100644 index 0000000..17b862b --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/recommend/components/FieldEnvironmentOverview.tsx @@ -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 ( + +

+ + {currentResult.fieldName} - 环境参数概览 +

+ +
+
+

+ + pH值 +

+

+ {currentResult.factors.find(f => f.id === 'ph')?.value.toFixed(1)} +

+

+ 最佳: {currentResult.factors.find(f => f.id === 'ph')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'ph')?.optimalRange[1]} +

+
+ +
+

+ + 有机质 +

+

+ {currentResult.factors.find(f => f.id === 'organic')?.value.toFixed(1)} g/kg +

+

+ 最佳: {currentResult.factors.find(f => f.id === 'organic')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'organic')?.optimalRange[1]} g/kg +

+
+ +
+

土层厚度

+

+ {currentResult.factors.find(f => f.id === 'depth')?.value.toFixed(0)} cm +

+

+ 最佳: {currentResult.factors.find(f => f.id === 'depth')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'depth')?.optimalRange[1]} cm +

+
+ +
+

+ + 排水性 +

+

+ {currentResult.factors.find(f => f.id === 'drainage')?.value.toFixed(0)}分 +

+

+ 最佳: {currentResult.factors.find(f => f.id === 'drainage')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'drainage')?.optimalRange[1]} 分 +

+
+
+ +
+
+

全氮

+

{currentResult.factors.find(f => f.id === 'nitrogen')?.value.toFixed(2)} g/kg

+

+ 最佳: {currentResult.factors.find(f => f.id === 'nitrogen')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'nitrogen')?.optimalRange[1]} g/kg +

+
+
+

全磷

+

{currentResult.factors.find(f => f.id === 'phosphorus')?.value.toFixed(2)} g/kg

+

+ 最佳: {currentResult.factors.find(f => f.id === 'phosphorus')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'phosphorus')?.optimalRange[1]} g/kg +

+
+
+

全钾

+

{currentResult.factors.find(f => f.id === 'potassium')?.value.toFixed(1)} g/kg

+

+ 最佳: {currentResult.factors.find(f => f.id === 'potassium')?.optimalRange[0]}-{currentResult.factors.find(f => f.id === 'potassium')?.optimalRange[1]} g/kg +

+
+
+ +
+

地块适宜性分析

+
+
+

+ 综合评分: + + {currentResult.totalScore}分 + +

+ + {currentResult.grade} + +
+
+

+ 优秀因子: + + {currentResult.factors.filter(f => f.score >= 80).length}项 + +

+

+ 待改善因子: + + {currentResult.factors.filter(f => f.score < 70).length}项 + +

+
+
+

+ 评价时间: + {currentResult.timestamp} +

+

+ 地块ID: + {currentResult.fieldId} +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/recommend/components/KnowledgeBaseDialog.tsx b/crop-x/src/app/(app)/land-information/suitability/recommend/components/KnowledgeBaseDialog.tsx new file mode 100644 index 0000000..c78e041 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/recommend/components/KnowledgeBaseDialog.tsx @@ -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 ( + + + + + + 作物-环境知识库 + + + 查看各种作物的土壤环境要求、气候要求、预期产量及风险因子 + + + +
+ {/* 知识库统计 */} +
+ +
+
+

作物总数

+

{cropKnowledgeBase.length}

+
+ +
+
+ +
+
+

粮食作物

+

+ {cropKnowledgeBase.filter(c => c.category === '粮食作物').length} +

+
+ +
+
+ +
+
+

经济作物

+

+ {cropKnowledgeBase.filter(c => c.category === '经济作物').length} +

+
+ +
+
+ +
+
+

蔬菜作物

+

+ {cropKnowledgeBase.filter(c => c.category === '蔬菜作物').length} +

+
+ +
+
+
+ + {/* 作物列表 */} +
+ {cropKnowledgeBase.map((crop) => ( + +
+
+
+

{crop.cropName}

+ + {crop.category} + +
+

{crop.description}

+
+
+

生长周期

+

{crop.growthCycle.days}天

+

+ {crop.growthCycle.seasons.join('、')} +

+
+
+ +
+ {/* 土壤环境要求 */} +
+

+ + 土壤环境要求 +

+
+
+ pH值: + + 最佳 {crop.soilRequirements.ph.optimal[0]}-{crop.soilRequirements.ph.optimal[1]} + + (可接受 {crop.soilRequirements.ph.acceptable[0]}-{crop.soilRequirements.ph.acceptable[1]}) + + +
+
+ 有机质: + + {crop.soilRequirements.organicMatter.optimal[0]}-{crop.soilRequirements.organicMatter.optimal[1]} g/kg + +
+
+ 土层厚度: + + {crop.soilRequirements.soilDepth.optimal[0]}-{crop.soilRequirements.soilDepth.optimal[1]} cm + +
+
+ 全氮: + + {crop.soilRequirements.nitrogen.optimal[0]}-{crop.soilRequirements.nitrogen.optimal[1]} g/kg + +
+
+ 全磷: + + {crop.soilRequirements.phosphorus.optimal[0]}-{crop.soilRequirements.phosphorus.optimal[1]} g/kg + +
+
+ 全钾: + + {crop.soilRequirements.potassium.optimal[0]}-{crop.soilRequirements.potassium.optimal[1]} g/kg + +
+
+ 排水性评分: + + {crop.soilRequirements.drainage.optimal[0]}-{crop.soilRequirements.drainage.optimal[1]} 分 + +
+
+
+ + {/* 气候要求与产量 */} +
+

+ + 气候要求 +

+
+
+ + + 温度: + + + {crop.climateRequirements.temperature.optimal[0]}-{crop.climateRequirements.temperature.optimal[1]} °C + +
+
+ + + 降雨量: + + + {crop.climateRequirements.rainfall.optimal[0]}-{crop.climateRequirements.rainfall.optimal[1]} mm/年 + +
+
+ + + 光照: + + + {crop.climateRequirements.sunlight.optimal[0]}-{crop.climateRequirements.sunlight.optimal[1]} 小时/天 + +
+
+ +

+ + 预期产量 (kg/亩) +

+
+
+ 高适宜性: + + {crop.expectedYield.high[0]}-{crop.expectedYield.high[1]} + +
+
+ 中等适宜性: + + {crop.expectedYield.medium[0]}-{crop.expectedYield.medium[1]} + +
+
+ 低适宜性: + + {crop.expectedYield.low[0]}-{crop.expectedYield.low[1]} + +
+
+
+
+ + {/* 风险因子 */} + {crop.riskFactors.length > 0 && ( +
+

+ + 主要风险因子 +

+
+ {crop.riskFactors.map((risk) => ( +
+
+

+ {risk.name} +

+ + {risk.severity === 'high' ? '高风险' : risk.severity === 'medium' ? '中风险' : '低风险'} + +
+

+ 触发条件: {risk.condition} +

+

+ 应对建议: {risk.suggestion} +

+
+ ))} +
+
+ )} +
+ ))} +
+
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/recommend/components/cropRecommendReducer.tsx b/crop-x/src/app/(app)/land-information/suitability/recommend/components/cropRecommendReducer.tsx new file mode 100644 index 0000000..12e7e1f --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/recommend/components/cropRecommendReducer.tsx @@ -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 }; \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/recommend/page.tsx b/crop-x/src/app/(app)/land-information/suitability/recommend/page.tsx new file mode 100644 index 0000000..21b6c4e --- /dev/null +++ b/crop-x/src/app/(app)/land-information/suitability/recommend/page.tsx @@ -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 ( +
+
+
+

作物适配推荐

+

+ 智能作物推荐清单、基于知识库的精准匹配 +

+
+
+ +
+
+ + {/* 地块选择和适宜性概览 */} +
+
+
+
+ + +
+ + {/* 适宜性评分卡片 */} +
+
+ +

适宜性评分

+

{currentResult.totalScore}

+ + {currentResult.grade} + +
+
+ + {/* 因子评分统计 */} +
+

因子评分统计

+
+
+ 优秀因子 + + {currentResult.factors.filter(f => f.score >= 80).length} + +
+
+ 待改善因子 + + {currentResult.factors.filter(f => f.score < 70).length} + +
+
+ 评价时间 + {currentResult.timestamp} +
+
+
+
+
+ +
+ +
+
+ + {/* 智能作物推荐 */} + + + {/* 知识库对话框 */} + + + {/* 系统说明 */} +
+
+
+ +
+
+

作物-环境知识库匹配系统

+
+
+

🌾 知识库构成

+
    +
  • 作物种类: 多种主要农作物
  • +
  • 土壤参数: pH、有机质等7项指标
  • +
  • 气候参数: 温度、降雨、光照
  • +
  • 最佳范围: 每个因子的要求范围
  • +
+
+
+

🎯 智能匹配流程

+
    +
  • 参数对比: 地块值与要求对比
  • +
  • 评分计算: 最佳100分,可接受60分
  • +
  • 综合匹配度: 加权平均所有因子
  • +
  • 分级推荐: ≥85高度推荐,70-84推荐
  • +
+
+
+

📊 产量预测机制

+
    +
  • 三级产量区间: 高/中/低适宜性
  • +
  • 历史数据支撑: 基于实测数据模型
  • +
  • 动态调整: 根据匹配度选择区间
  • +
  • 品种差异: 考虑作物产量特性
  • +
+
+
+

⚠️ 风险识别系统

+
    +
  • 多维度监测: 土壤、气候、营养
  • +
  • 条件触发: 自动判断风险存在
  • +
  • 严重度分级: 高/中/低风险等级
  • +
  • 应对建议: 每个风险配套措施
  • +
+
+
+ +
+

+ 💡 系统优势: + 本系统整合了土壤科学、作物学、气象学等多学科知识,建立了完整的作物-环境适配知识库。 + 通过科学的评分体系和智能匹配算法,为农业生产决策提供可靠的数据支持,帮助优化作物布局, + 提高土地利用率和经济效益,同时有效规避种植风险。 +

+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/suitability/weight/page.tsx b/crop-x/src/app/(app)/land-information/suitability/weight/page.tsx deleted file mode 100644 index 456f585..0000000 --- a/crop-x/src/app/(app)/land-information/suitability/weight/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Card } from '@/components/ui/card'; - -export default function WeightPage() { - return ( -
- -

权重设置

-
-

- 页面路径: /land-information/suitability/weight -

-
-
-
- ); -} \ No newline at end of file diff --git a/crop-x/src/app/layout.tsx b/crop-x/src/app/layout.tsx index 2073c90..cdcdbc0 100644 --- a/crop-x/src/app/layout.tsx +++ b/crop-x/src/app/layout.tsx @@ -318,17 +318,17 @@ const fieldMessageManagement = { items: [ { title: "多因子综合评价", - url: "/land-information/suitability/comprehensive", + url: "/land-information/suitability/multiFactor", isActive: false }, { title: "自动化空间分析", - url: "/land-information/suitability/batch", + url: "/land-information/suitability/auto", isActive: false }, { title: "作物适配推荐", - url: "/land-information/suitability/crop", + url: "/land-information/suitability/recommend", isActive: false } ] diff --git a/crop-x/temp_file.tsx b/crop-x/temp_file.tsx new file mode 100644 index 0000000..78434b8 --- /dev/null +++ b/crop-x/temp_file.tsx @@ -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({ + total: 0, + processed: 0, + highSuitability: 0, + mediumSuitability: 0, + lowSuitability: 0, + currentField: '', + }); + const [isBatchRunning, setIsBatchRunning] = useState(false); + const [batchAnalysisResults, setBatchAnalysisResults] = useState([]); + + // 评价因子权重配置 + const [factorWeights, setFactorWeights] = useState([ + { 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( + 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 ( +
+
+
+

多因子综合评价

+

+ 地块适宜性评价、自动化空间分析与作物适配推荐 +

+
+
+ + +
+
+ + {/* 多因子综合评价 */} +
+ +
+
+ + +
+ +
+ +
+
+
+ + {/* 评价结果总览 */} +
+ +
+ +

综合评分

+

+ {currentResult.totalScore} +

+ + {currentResult.grade} + +
+
+ + +
+ +

优秀因子

+

+ {currentResult.factors.filter(f => f.score >= 80).length} +

+

+ / {currentResult.factors.length} 项 +

+
+
+ + +
+ +

待改善因子

+

+ {currentResult.factors.filter(f => f.score < 70).length} +

+

需要优化

+
+
+ + +
+ +

评价时间

+

+ {formatDate(currentResult.timestamp)} +

+

最近更新

+
+
+
+ + {/* 因子详细评分 */} + +

评价因子详细分析

+
+ {currentResult.factors.map((factor) => ( +
+
+
+ {factor.name} + + 权重: {factor.weight}% + +
+
+ + 实际值: {factor.value.toFixed(1)}{factor.unit} + + + 最佳范围: {factor.optimalRange[0]}-{factor.optimalRange[1]}{factor.unit} + + + {factor.score}分 + +
+
+
+ +
+
+
+
+
+
+ ))} +
+ + + {/* 加权计算说明 */} + +

层次分析法(AHP)加权计算

+
+
+

计算公式:

+ + 总分 = Σ(因子得分 × 因子权重) + +

+ = ({currentResult.factors.map((f, i) => + `${f.score} × ${f.weight}%${i < currentResult.factors.length - 1 ? ' + ' : ''}` + ).join('')}) +

+

+ = {currentResult.totalScore}分 +

+
+ +
+
+

高度适宜 (≥80分)

+
    +
  • • 土壤条件优秀
  • +
  • • 适合多种作物
  • +
  • • 预期产量高
  • +
+
+
+

一般适宜 (60-79分)

+
    +
  • • 部分因子需改善
  • +
  • • 可种植耐性作物
  • +
  • • 需适当改良
  • +
+
+
+

不适宜 (<60分)

+
    +
  • • 土壤条件较差
  • +
  • • 需大幅改良
  • +
  • • 风险较高
  • +
+
+
+
+
+ + {/* 多因子综合评价功能说明 */} + +
+ +
+

多因子综合评价功能说明:

+
    +
  • 关键指标整合: 整合pH值、有机质含量、土层厚度、全氮、全磷、全钾、排水性等7项关键土壤指标
  • +
  • 层次分析法(AHP): 采用层次分析法构建加权评分体系,公式:总分 = Σ(因子得分 × 因子权重)
  • +
  • 单因子评分: 根据实际值与最佳范围的接近程度计算各因子得分(0-100分),范围内得分85-100分
  • +
  • 自定义权重: 支持手动调整各指标权重,提供粮食作物和经济作物两种预设方案
  • +
  • 综合得分: 输出0-100分的适宜性评分,自动计算加权总分
  • +
  • 三级分级: 高度适宜(≥80分)、一般适宜(60-79分)、不适宜(<60分)
  • +
  • 可视化展示: 提供进度条、颜色标识等多维度可视化,直观展示各因子评分与最佳范围对比
  • +
  • 改善建议: 自动识别待改善因子,为土壤改良提供决策依据
  • +
+
+
+
+ +