diff --git a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/ControlPanel.tsx b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/ControlPanel.tsx
new file mode 100644
index 0000000..47d84ff
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/ControlPanel.tsx
@@ -0,0 +1,141 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Slider } from '@/components/ui/slider';
+import { Box, Layers, Grid3x3, ZoomIn, ZoomOut } from 'lucide-react';
+
+interface ControlPanelProps {
+ selectedField: string;
+ selectedNutrient: 'organic' | 'nitrogen' | 'phosphorus' | 'potassium';
+ viewMode: '3d' | 'slice' | 'contour';
+ depthSlice: number[];
+ zoomLevel: number;
+ onFieldChange: (value: string) => void;
+ onNutrientChange: (value: 'organic' | 'nitrogen' | 'phosphorus' | 'potassium') => void;
+ onViewModeChange: (value: '3d' | 'slice' | 'contour') => void;
+ onDepthSliceChange: (value: number[]) => void;
+ onZoomIn: () => void;
+ onZoomOut: () => void;
+}
+
+export function ControlPanel({
+ selectedField,
+ selectedNutrient,
+ viewMode,
+ depthSlice,
+ zoomLevel,
+ onFieldChange,
+ onNutrientChange,
+ onViewModeChange,
+ onDepthSliceChange,
+ onZoomIn,
+ onZoomOut,
+}: ControlPanelProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {zoomLevel}%
+
+
+
+
+ {viewMode === 'slice' && (
+
+
+
+
+ 0cm
+ {depthSlice[0]}cm
+ 60cm
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/DataAnalysis.tsx b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/DataAnalysis.tsx
new file mode 100644
index 0000000..bd8f4e9
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/DataAnalysis.tsx
@@ -0,0 +1,131 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Activity, TrendingUp } from 'lucide-react';
+
+interface DataAnalysisProps {
+ selectedNutrient: 'organic' | 'nitrogen' | 'phosphorus' | 'potassium';
+}
+
+export function DataAnalysis({ selectedNutrient }: DataAnalysisProps) {
+ const nutrientConfig = {
+ organic: { label: '有机质', unit: 'g/kg', color: '#22c55e', min: 0, max: 40 },
+ nitrogen: { label: '全氮', unit: 'g/kg', color: '#3b82f6', min: 0, max: 2 },
+ phosphorus: { label: '有效磷', unit: 'mg/kg', color: '#f59e0b', min: 0, max: 40 },
+ potassium: { label: '速效钾', unit: 'mg/kg', color: '#a855f7', min: 0, max: 250 },
+ };
+
+ const currentNutrient = nutrientConfig[selectedNutrient];
+
+ const layers3DData = [
+ { depth: '0-20cm', avgValue: 28.5, distribution: [25, 28, 32, 27, 30, 26, 29, 31, 28, 27] },
+ { depth: '20-40cm', avgValue: 20.2, distribution: [18, 21, 23, 19, 22, 20, 21, 22, 19, 20] },
+ { depth: '40-60cm', avgValue: 13.5, distribution: [12, 14, 15, 13, 14, 12, 13, 15, 14, 13] },
+ ];
+
+ const getColorForValue = (value: number, nutrient: typeof selectedNutrient) => {
+ const config = nutrientConfig[nutrient];
+ const ratio = (value - config.min) / (config.max - config.min);
+
+ if (ratio < 0.2) return '#ef4444';
+ if (ratio < 0.4) return '#f97316';
+ if (ratio < 0.6) return '#eab308';
+ if (ratio < 0.8) return '#84cc16';
+ return '#22c55e';
+ };
+
+ return (
+
+
+
+
+ {layers3DData.map((layer, index) => (
+
+
+ {layer.depth}
+
+ {layer.avgValue} {currentNutrient.unit}
+
+
+
+
+ ))}
+
+
+
+
+
+ 递减率:
+ -29.1%/20cm
+
+
+ 表层富集:
+ 明显
+
+
+
+
+
+
+
+
+
水平变异性
+
+
+
+ 变异系数:
+ 14.8%
+
+
+ 变异等级:
+ 中等变异
+
+
+ 最高值:
+ 32.1 {currentNutrient.unit}
+
+
+ 最低值:
+ 22.3 {currentNutrient.unit}
+
+
+ 极差:
+ 9.8 {currentNutrient.unit}
+
+
+
+
+
+ 插值算法
+
+
• 克里金插值(Kriging)
+
• 考虑空间自相关性
+
• 最佳无偏估计
+
• 适用于土壤养分空间预测
+
+
+
+
+ 分析结论
+
+ - • 养分在垂直方向呈递减分布
+ - • 表层(0-20cm)含量最高
+ - • 水平分布呈中等变异
+ - • 建议分层精准施肥
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/DataTable.tsx b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/DataTable.tsx
new file mode 100644
index 0000000..28b7ca8
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/DataTable.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+
+export function DataTable() {
+ return (
+
+ 分层数据详细对比
+
+
+
+
+ | 土层深度 |
+ 有机质 (g/kg) |
+ 全氮 (g/kg) |
+ 有效磷 (mg/kg) |
+ 速效钾 (mg/kg) |
+ 平均pH值 |
+ 含水量 (%) |
+
+
+
+
+ | 0-20cm |
+ 28.5 |
+ 1.2 |
+ 25.3 |
+ 180 |
+ 6.8 |
+ 22.5 |
+
+
+ | 20-40cm |
+ 20.2 |
+ 0.8 |
+ 18.5 |
+ 145 |
+ 6.5 |
+ 25.8 |
+
+
+ | 40-60cm |
+ 13.5 |
+ 0.5 |
+ 12.3 |
+ 98 |
+ 6.3 |
+ 28.2 |
+
+
+ | 层间变化率 |
+ -29.1% |
+ -33.3% |
+ -26.8% |
+ -19.4% |
+ -7.4% |
+ +25.3% |
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/UsageGuide.tsx b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/UsageGuide.tsx
new file mode 100644
index 0000000..d160545
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/UsageGuide.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { AlertCircle } from 'lucide-react';
+
+export function UsageGuide() {
+ return (
+
+
+
+
+
三维可视化分析说明:
+
+ - • 三维模型: 立体展示土壤养分的空间分布,支持旋转和缩放
+ - • 立体切片: 选择任意深度查看该层的水平分布热力图
+ - • 等值面图: 以等值线形式展示养分浓度分布
+ - • 克里金插值: 基于采样点数据,使用地统计学方法生成连续分布模型
+ - • 垂直分析: 分析养分在土壤剖面的递减规律
+ - • 水平变异: 评估同一深度养分分布的均匀性
+ - • 精准施肥: 根据三维分布特征制定分层分区施肥方案
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/Visualization3D.tsx b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/Visualization3D.tsx
new file mode 100644
index 0000000..5fd9bed
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/Visualization3D.tsx
@@ -0,0 +1,260 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Box, Layers, Grid3x3, Eye, RotateCw, ZoomIn, ZoomOut } from 'lucide-react';
+
+interface Visualization3DProps {
+ viewMode: '3d' | 'slice' | 'contour';
+ selectedNutrient: 'organic' | 'nitrogen' | 'phosphorus' | 'potassium';
+ depthSlice: number[];
+ rotationAngle: number;
+ zoomLevel: number;
+ onRotate: () => void;
+ onZoomIn: () => void;
+ onZoomOut: () => void;
+}
+
+export function Visualization3D({
+ viewMode,
+ selectedNutrient,
+ depthSlice,
+ rotationAngle,
+ zoomLevel,
+ onRotate,
+ onZoomIn,
+ onZoomOut,
+}: Visualization3DProps) {
+ const nutrientConfig = {
+ organic: { label: '有机质', unit: 'g/kg', color: '#22c55e', min: 0, max: 40 },
+ nitrogen: { label: '全氮', unit: 'g/kg', color: '#3b82f6', min: 0, max: 2 },
+ phosphorus: { label: '有效磷', unit: 'mg/kg', color: '#f59e0b', min: 0, max: 40 },
+ potassium: { label: '速效钾', unit: 'mg/kg', color: '#a855f7', min: 0, max: 250 },
+ };
+
+ const currentNutrient = nutrientConfig[selectedNutrient];
+
+ const layers3DData = [
+ { depth: '0-20cm', avgValue: 28.5, distribution: [25, 28, 32, 27, 30, 26, 29, 31, 28, 27] },
+ { depth: '20-40cm', avgValue: 20.2, distribution: [18, 21, 23, 19, 22, 20, 21, 22, 19, 20] },
+ { depth: '40-60cm', avgValue: 13.5, distribution: [12, 14, 15, 13, 14, 12, 13, 15, 14, 13] },
+ ];
+
+ const getColorForValue = (value: number, nutrient: typeof selectedNutrient) => {
+ const config = nutrientConfig[nutrient];
+ const ratio = (value - config.min) / (config.max - config.min);
+
+ if (ratio < 0.2) return '#ef4444';
+ if (ratio < 0.4) return '#f97316';
+ if (ratio < 0.6) return '#eab308';
+ if (ratio < 0.8) return '#84cc16';
+ return '#22c55e';
+ };
+
+ return (
+
+
+
+
+
+ {viewMode === '3d' && '三维分布模型'}
+ {viewMode === 'slice' && '立体切片图'}
+ {viewMode === 'contour' && '等值面图'}
+
+
+
{currentNutrient.label}
+
+
+
+ {viewMode === '3d' && (
+
+
+ {layers3DData.map((layer, layerIndex) => {
+ const offsetY = layerIndex * 60;
+ return (
+
+
+ {layer.distribution.map((value, cellIndex) => (
+
+ ))}
+
+
+
+ {layer.depth}
+
+
+ );
+ })}
+
+
+ 水平方向 →
+
+
+ 深度 ↓
+
+
+
+ )}
+
+ {viewMode === 'slice' && (
+
+
+
选择切片深度
+
+ {depthSlice[0]}cm
+
+
+
+ {Array.from({ length: 25 }).map((_, index) => {
+ const layerIndex = Math.floor(depthSlice[0] / 20);
+ const baseValue = layers3DData[layerIndex]?.avgValue || 15;
+ const variation = (Math.sin(index * 0.5) * 5);
+ const value = baseValue + variation;
+
+ return (
+
+
+ {value.toFixed(1)}
+
+
+ );
+ })}
+
+
+
+ {depthSlice[0]}cm 深度的水平切片
+
+
+
+ )}
+
+ {viewMode === 'contour' && (
+
+
+
+
+ 表层(0-20cm)等值线分布图
+
+
+
+ )}
+
+
+
+
+ 视角: {rotationAngle}°
+
+
+
+
+
+
+
+
+
+
+
+
+ 养分含量图例
+ 单位: {currentNutrient.unit}
+
+
+
{currentNutrient.min}
+
+
{currentNutrient.max}
+
+
+ 很低
+ 低
+ 中等
+ 较高
+ 高
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/layerSamplingReducer.tsx b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/layerSamplingReducer.tsx
new file mode 100644
index 0000000..4b7ca0f
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/components/layerSamplingReducer.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import { ReactNode } from 'react';
+
+export interface LayerSamplingState {
+ selectedField: string;
+ selectedNutrient: 'organic' | 'nitrogen' | 'phosphorus' | 'potassium';
+ viewMode: '3d' | 'slice' | 'contour';
+ depthSlice: number[];
+ rotationAngle: number;
+ zoomLevel: number;
+}
+
+export type LayerSamplingAction =
+ | { type: 'SET_SELECTED_FIELD'; payload: string }
+ | { type: 'SET_SELECTED_NUTRIENT'; payload: 'organic' | 'nitrogen' | 'phosphorus' | 'potassium' }
+ | { type: 'SET_VIEW_MODE'; payload: '3d' | 'slice' | 'contour' }
+ | { type: 'SET_DEPTH_SLICE'; payload: number[] }
+ | { type: 'SET_ROTATION_ANGLE'; payload: number }
+ | { type: 'SET_ZOOM_LEVEL'; payload: number }
+ | { type: 'ROTATE' }
+ | { type: 'ZOOM_IN' }
+ | { type: 'ZOOM_OUT' };
+
+export const initialState: LayerSamplingState = {
+ selectedField: 'field-1',
+ selectedNutrient: 'organic',
+ viewMode: '3d',
+ depthSlice: [20],
+ rotationAngle: 45,
+ zoomLevel: 100,
+};
+
+export function layerSamplingReducer(state: LayerSamplingState, action: LayerSamplingAction): LayerSamplingState {
+ switch (action.type) {
+ case 'SET_SELECTED_FIELD':
+ return { ...state, selectedField: action.payload };
+
+ case 'SET_SELECTED_NUTRIENT':
+ return { ...state, selectedNutrient: action.payload };
+
+ case 'SET_VIEW_MODE':
+ return { ...state, viewMode: action.payload };
+
+ case 'SET_DEPTH_SLICE':
+ return { ...state, depthSlice: action.payload };
+
+ case 'SET_ROTATION_ANGLE':
+ return { ...state, rotationAngle: action.payload };
+
+ case 'SET_ZOOM_LEVEL':
+ return { ...state, zoomLevel: action.payload };
+
+ case 'ROTATE':
+ return { ...state, rotationAngle: (state.rotationAngle + 45) % 360 };
+
+ case 'ZOOM_IN':
+ return { ...state, zoomLevel: Math.min(200, state.zoomLevel + 10) };
+
+ case 'ZOOM_OUT':
+ return { ...state, zoomLevel: Math.max(50, state.zoomLevel - 10) };
+
+ default:
+ return state;
+ }
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/page.tsx b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/page.tsx
index c1fdc1e..a872b5c 100644
--- a/crop-x/src/app/(app)/land-information/analysis/layer-sampling/page.tsx
+++ b/crop-x/src/app/(app)/land-information/analysis/layer-sampling/page.tsx
@@ -1,18 +1,107 @@
'use client';
+import { useReducer } from 'react';
+import { toast } from 'sonner';
import { Card } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { RotateCw } from 'lucide-react';
+import {
+ layerSamplingReducer,
+ initialState,
+ LayerSamplingState,
+ LayerSamplingAction
+} from './components/layerSamplingReducer';
+import { ControlPanel } from './components/ControlPanel';
+import { Visualization3D } from './components/Visualization3D';
+import { DataAnalysis } from './components/DataAnalysis';
+import { DataTable } from './components/DataTable';
+import { UsageGuide } from './components/UsageGuide';
export default function LayerSamplingPage() {
+ const [state, dispatch] = useReducer(layerSamplingReducer, initialState);
+
+ const handleFieldChange = (value: string) => {
+ dispatch({ type: 'SET_SELECTED_FIELD', payload: value });
+ };
+
+ const handleNutrientChange = (value: 'organic' | 'nitrogen' | 'phosphorus' | 'potassium') => {
+ dispatch({ type: 'SET_SELECTED_NUTRIENT', payload: value });
+ };
+
+ const handleViewModeChange = (value: '3d' | 'slice' | 'contour') => {
+ dispatch({ type: 'SET_VIEW_MODE', payload: value });
+ };
+
+ const handleDepthSliceChange = (value: number[]) => {
+ dispatch({ type: 'SET_DEPTH_SLICE', payload: value });
+ };
+
+ const handleRotate = () => {
+ dispatch({ type: 'ROTATE' });
+ toast.success(`旋转至 ${state.rotationAngle}°`);
+ };
+
+ const handleZoomIn = () => {
+ dispatch({ type: 'ZOOM_IN' });
+ };
+
+ const handleZoomOut = () => {
+ dispatch({ type: 'ZOOM_OUT' });
+ };
+
return (
-
- 分层采样分析
-
-
- 页面路径: /land-information/analysis/layer-sampling
+
+
+
分层采样分析
+
+ 三维可视化展示土壤养分在垂直和水平方向的分布规律
-
+
+
+
+
+
+
+
+
+
+
+
+
);
}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/AddSamplePointDialog.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/AddSamplePointDialog.tsx
new file mode 100644
index 0000000..b888ce3
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/AddSamplePointDialog.tsx
@@ -0,0 +1,338 @@
+'use client';
+
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+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, Save, Droplets, Leaf, Zap, TrendingUp } from 'lucide-react';
+import { SoilDataState, SoilDataAction } from './soilDataReducer';
+
+interface AddSamplePointDialogProps {
+ state: SoilDataState;
+ dispatch: React.Dispatch;
+}
+
+export default function AddSamplePointDialog({ state, dispatch }: AddSamplePointDialogProps) {
+ const handleAddSamplePoint = () => {
+ const newPoint = state.newPoint;
+
+ if (!newPoint.code || !newPoint.fieldName || !newPoint.sampleDate || !newPoint.sampler) {
+ return;
+ }
+
+ if (!newPoint.latitude || !newPoint.longitude || newPoint.latitude === 0 || newPoint.longitude === 0) {
+ return;
+ }
+
+ // 从设备读取数据并填充到分层信息
+ const layersWithData = (newPoint.layers || []).map(layer => {
+ if (layer.deviceId) {
+ const device = state.iotDevices.find(d => d.id === layer.deviceId);
+ if (device) {
+ return {
+ ...layer,
+ pH: device.data.pH,
+ organicMatter: device.data.organicMatter,
+ nitrogen: device.data.nitrogen,
+ phosphorus: device.data.phosphorus,
+ potassium: device.data.potassium,
+ moisture: device.data.moisture,
+ };
+ }
+ }
+ return layer;
+ });
+
+ const samplePoint = {
+ id: `sp-${Date.now()}`,
+ ...newPoint,
+ layers: layersWithData,
+ } as any;
+
+ dispatch({ type: 'ADD_SAMPLE_POINT', payload: samplePoint });
+ dispatch({ type: 'SET_SHOW_ADD_DIALOG', payload: false });
+ dispatch({ type: 'RESET_NEW_POINT' });
+ };
+
+ const handleMapPointSelect = (lat: number, lng: number) => {
+ dispatch({ type: 'UPDATE_NEW_POINT', payload: { latitude: lat, longitude: lng } });
+ };
+
+ const handleAddLayer = () => {
+ const currentLayers = state.newPoint.layers || [];
+ const newLayer = {
+ depth: `${currentLayers.length * 20}-${(currentLayers.length + 1) * 20}cm`,
+ deviceId: '',
+ };
+ dispatch({
+ type: 'UPDATE_NEW_POINT',
+ payload: {
+ layers: [...currentLayers, newLayer]
+ }
+ });
+ };
+
+ const handleUpdateLayerDevice = (layerIndex: number, deviceId: string) => {
+ const updatedLayers = [...(state.newPoint.layers || [])];
+ updatedLayers[layerIndex] = { ...updatedLayers[layerIndex], deviceId };
+ dispatch({
+ type: 'UPDATE_NEW_POINT',
+ payload: { layers: updatedLayers }
+ });
+ };
+
+ const handleRemoveLayer = (index: number) => {
+ const currentLayers = state.newPoint.layers || [];
+ if (currentLayers.length > 1) {
+ const updatedLayers = currentLayers.filter((_, i) => i !== index);
+ dispatch({
+ type: 'UPDATE_NEW_POINT',
+ payload: { layers: updatedLayers }
+ });
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/DeleteConfirmDialog.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/DeleteConfirmDialog.tsx
new file mode 100644
index 0000000..a0a8453
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/DeleteConfirmDialog.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
+import { SoilDataState, SoilDataAction } from './soilDataReducer';
+
+interface DeleteConfirmDialogProps {
+ state: SoilDataState;
+ dispatch: React.Dispatch;
+}
+
+export default function DeleteConfirmDialog({ state, dispatch }: DeleteConfirmDialogProps) {
+ const handleConfirmDelete = () => {
+ if (state.pointToDelete) {
+ dispatch({ type: 'DELETE_SAMPLE_POINT', payload: state.pointToDelete });
+ dispatch({ type: 'SET_POINT_TO_DELETE', payload: null });
+ dispatch({ type: 'SET_SHOW_DELETE_DIALOG', payload: false });
+ }
+ };
+
+ const handleCancel = () => {
+ dispatch({ type: 'SET_POINT_TO_DELETE', payload: null });
+ dispatch({ type: 'SET_SHOW_DELETE_DIALOG', payload: false });
+ };
+
+ const pointToDelete = state.samplePoints.find(p => p.id === state.pointToDelete);
+
+ return (
+ {
+ if (!open) {
+ handleCancel();
+ }
+ }}>
+
+
+ 确认删除采样点
+
+ 确定要删除采样点{pointToDelete?.code}吗?
+ {pointToDelete && (
+
+ 位置:{pointToDelete.fieldName} | 采样日期:{pointToDelete.sampleDate}
+
+ )}
+ 此操作将删除该采样点的所有数据,包括分层信息和理化指标,且无法恢复。
+
+
+
+
+ 取消
+
+
+ 确认删除
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/EditSamplePointDialog.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/EditSamplePointDialog.tsx
new file mode 100644
index 0000000..6bb900f
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/EditSamplePointDialog.tsx
@@ -0,0 +1,344 @@
+'use client';
+
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+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, Save, Droplets, Leaf, Zap, TrendingUp } from 'lucide-react';
+import { SoilDataState, SoilDataAction } from './soilDataReducer';
+
+interface EditSamplePointDialogProps {
+ state: SoilDataState;
+ dispatch: React.Dispatch;
+}
+
+export default function EditSamplePointDialog({ state, dispatch }: EditSamplePointDialogProps) {
+ const handleUpdateSamplePoint = () => {
+ const newPoint = state.newPoint;
+
+ if (!newPoint.code || !newPoint.fieldName || !newPoint.sampleDate || !newPoint.sampler) {
+ return;
+ }
+
+ if (!newPoint.latitude || !newPoint.longitude || newPoint.latitude === 0 || newPoint.longitude === 0) {
+ return;
+ }
+
+ if (!state.selectedPoint) return;
+
+ // 从设备读取数据并填充到分层信息
+ const layersWithData = (newPoint.layers || []).map(layer => {
+ if (layer.deviceId) {
+ const device = state.iotDevices.find(d => d.id === layer.deviceId);
+ if (device) {
+ return {
+ ...layer,
+ pH: device.data.pH,
+ organicMatter: device.data.organicMatter,
+ nitrogen: device.data.nitrogen,
+ phosphorus: device.data.phosphorus,
+ potassium: device.data.potassium,
+ moisture: device.data.moisture,
+ };
+ }
+ }
+ return layer;
+ });
+
+ const updatedPoint = {
+ ...state.selectedPoint,
+ ...newPoint,
+ layers: layersWithData,
+ };
+
+ dispatch({ type: 'UPDATE_SAMPLE_POINT', payload: updatedPoint });
+ dispatch({ type: 'SET_SHOW_EDIT_DIALOG', payload: false });
+ dispatch({ type: 'SET_SELECTED_POINT', payload: null });
+ dispatch({ type: 'RESET_NEW_POINT' });
+ };
+
+ const handleMapPointSelect = (lat: number, lng: number) => {
+ dispatch({ type: 'UPDATE_NEW_POINT', payload: { latitude: lat, longitude: lng } });
+ };
+
+ const handleAddLayer = () => {
+ const currentLayers = state.newPoint.layers || [];
+ const newLayer = {
+ depth: `${currentLayers.length * 20}-${(currentLayers.length + 1) * 20}cm`,
+ deviceId: '',
+ };
+ dispatch({
+ type: 'UPDATE_NEW_POINT',
+ payload: {
+ layers: [...currentLayers, newLayer]
+ }
+ });
+ };
+
+ const handleUpdateLayerDevice = (layerIndex: number, deviceId: string) => {
+ const updatedLayers = [...(state.newPoint.layers || [])];
+ updatedLayers[layerIndex] = { ...updatedLayers[layerIndex], deviceId };
+ dispatch({
+ type: 'UPDATE_NEW_POINT',
+ payload: { layers: updatedLayers }
+ });
+ };
+
+ const handleRemoveLayer = (index: number) => {
+ const currentLayers = state.newPoint.layers || [];
+ if (currentLayers.length > 1) {
+ const updatedLayers = currentLayers.filter((_, i) => i !== index);
+ dispatch({
+ type: 'UPDATE_NEW_POINT',
+ payload: { layers: updatedLayers }
+ });
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/LayerDataDialog.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/LayerDataDialog.tsx
new file mode 100644
index 0000000..4a074b1
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/LayerDataDialog.tsx
@@ -0,0 +1,165 @@
+'use client';
+
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Badge } from '@/components/ui/badge';
+import { SoilDataState, SoilDataAction, getPHLevel, getOrganicMatterLevel } from './soilDataReducer';
+
+interface LayerDataDialogProps {
+ state: SoilDataState;
+ dispatch: React.Dispatch;
+}
+
+export default function LayerDataDialog({ state, dispatch }: LayerDataDialogProps) {
+ if (!state.selectedPoint) return null;
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/ProfileInformation.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/ProfileInformation.tsx
new file mode 100644
index 0000000..96bc733
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/ProfileInformation.tsx
@@ -0,0 +1,194 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Badge } from '@/components/ui/badge';
+import { SamplePoint, getPHLevel, getOrganicMatterLevel } from './soilDataReducer';
+
+interface ProfileInformationProps {
+ samplePoints: SamplePoint[];
+}
+
+export default function ProfileInformation({ samplePoints }: ProfileInformationProps) {
+ const selectedPoint = samplePoints[0]; // 默认选择第一个采样点
+
+ if (!selectedPoint) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {/* 剖面可视化 */}
+
+ 土壤剖面可视化
+
+ {/* 左侧剖面图 */}
+
+
+ {selectedPoint.layers.map((layer, index) => (
+
+ ))}
+
+
深度剖面图
+
+
+ {/* 右侧数据表格 */}
+
+
+
+
+
+ | 深度 |
+ pH值 |
+ 有机质(g/kg) |
+ 全氮(g/kg) |
+ 有效磷(mg/kg) |
+ 速效钾(mg/kg) |
+ 含水量(%) |
+
+
+
+ {selectedPoint.layers.map((layer, index) => (
+
+ | {layer.depth} |
+
+
+ {layer.pH}
+
+ |
+
+
+ {layer.organicMatter}
+
+ |
+ {layer.nitrogen} |
+ {layer.phosphorus} |
+ {layer.potassium} |
+ {layer.moisture}% |
+
+ ))}
+
+
+
+
+ {/* 趋势分析 */}
+
+
剖面特征分析
+
+ - • pH值: 随深度增加{selectedPoint.layers[0]?.pH > (selectedPoint.layers[selectedPoint.layers.length - 1]?.pH || 0) ? '下降' : '上升'},表层为{getPHLevel(selectedPoint.layers[0]?.pH || 0).label}
+ - • 有机质: 表层含量高,向下递减,符合正常分布规律
+ - • 养分: 氮磷钾含量表层最高,20cm以下显著降低
+ - • 水分: 随深度增加含水量上升,底层保水性好
+
+
+
+
+
+
+ {/* 分层数据详情 */}
+
+ 分层数据详细记录
+
+ {selectedPoint.layers.map((layer, index) => (
+
+
+
第 {index + 1} 层
+ {layer.depth}
+
+
+
+
+
+
+ 有机质:
+ {layer.organicMatter} g/kg
+
+
+ 全氮:
+ {layer.nitrogen} g/kg
+
+
+
+
+
+ 有效磷:
+ {layer.phosphorus} mg/kg
+
+
+ 速效钾:
+ {layer.potassium} mg/kg
+
+
+ 含水量:
+ {layer.moisture}%
+
+
+
+
+ {/* 营养状况评估 */}
+
+
+ 营养状况:
+
+ {layer.organicMatter && layer.organicMatter > 25 ? '营养丰富' :
+ layer.organicMatter && layer.organicMatter > 15 ? '营养中等' : '营养偏低'}
+
+
+
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SamplePointsList.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SamplePointsList.tsx
new file mode 100644
index 0000000..cc3f68c
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SamplePointsList.tsx
@@ -0,0 +1,184 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Badge } from '@/components/ui/badge';
+import { Search, MapPin, Layers, Edit, Trash2, Leaf, Zap, TrendingUp, Droplets } from 'lucide-react';
+import { SoilDataState, SoilDataAction, getPHLevel } from './soilDataReducer';
+
+interface SamplePointsListProps {
+ state: SoilDataState;
+ dispatch: React.Dispatch;
+}
+
+export default function SamplePointsList({ state, dispatch }: SamplePointsListProps) {
+ const handleViewLayers = (point: any) => {
+ dispatch({ type: 'SET_SELECTED_POINT', payload: point });
+ dispatch({ type: 'SET_SHOW_LAYER_DIALOG', payload: true });
+ };
+
+ const handleEditPoint = (point: any) => {
+ dispatch({ type: 'SET_SELECTED_POINT', payload: point });
+ dispatch({ type: 'SET_NEW_POINT', payload: point });
+ dispatch({ type: 'SET_SHOW_EDIT_DIALOG', payload: true });
+ };
+
+ const handleDeleteClick = (pointId: string) => {
+ dispatch({ type: 'SET_POINT_TO_DELETE', payload: pointId });
+ dispatch({ type: 'SET_SHOW_DELETE_DIALOG', payload: true });
+ };
+
+ const handleSearchChange = (value: string) => {
+ dispatch({ type: 'SET_FILTERS', payload: { searchKeyword: value } });
+ };
+
+ const handleFieldChange = (value: string) => {
+ dispatch({ type: 'SET_FILTERS', payload: { selectedField: value } });
+ };
+
+ // 筛选采样点
+ const filteredPoints = state.samplePoints.filter(point => {
+ const matchesSearch = !state.filters.searchKeyword ||
+ point.code.toLowerCase().includes(state.filters.searchKeyword.toLowerCase()) ||
+ point.fieldName.toLowerCase().includes(state.filters.searchKeyword.toLowerCase());
+
+ const matchesField = state.filters.selectedField === 'all' ||
+ point.fieldName === state.filters.selectedField;
+
+ return matchesSearch && matchesField;
+ });
+
+ return (
+
+ {/* 搜索和筛选 */}
+
+
+
+
+
+ handleSearchChange(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+ {/* 采样点列表 */}
+
+ {filteredPoints.map((point) => (
+
+
+
+
+
+
+
+
{point.code}
+ {point.fieldName}
+
+
+ 坐标: {point.latitude.toFixed(6)}, {point.longitude.toFixed(6)}
+
+
+
+
+
+
+
采样日期
+
{point.sampleDate}
+
+
+
采样人
+
{point.sampler}
+
+
+
分层数
+
{point.layers.length} 层
+
+
+
表层pH值
+
+
{point.layers[0]?.pH}
+
+
+
+
+
+ {/* 表层指标快览 */}
+ {point.layers[0] && (
+
+
表层指标(0-20cm)
+
+
+
+ 有机质: {point.layers[0].organicMatter} g/kg
+
+
+
+ 全氮: {point.layers[0].nitrogen} g/kg
+
+
+
+ 有效磷: {point.layers[0].phosphorus} mg/kg
+
+
+
+ 速效钾: {point.layers[0].potassium} mg/kg
+
+
+
+ 含水量: {point.layers[0].moisture}%
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ ))}
+
+ {filteredPoints.length === 0 && (
+
+
+
+
暂无采样点数据
+
点击"新增采样点"开始添加数据
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SoilDataContent.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SoilDataContent.tsx
new file mode 100644
index 0000000..20020a7
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SoilDataContent.tsx
@@ -0,0 +1,215 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Download, FileText, Plus, MapPin, Layers, BarChart3, Droplets } from 'lucide-react';
+import { SoilDataState, SoilDataAction } from './soilDataReducer';
+import StatisticsCards from './StatisticsCards';
+import SamplePointsList from './SamplePointsList';
+import SpatialDistribution from './SpatialDistribution';
+import ProfileInformation from './ProfileInformation';
+import StatisticalAnalysis from './StatisticalAnalysis';
+import AddSamplePointDialog from './AddSamplePointDialog';
+import EditSamplePointDialog from './EditSamplePointDialog';
+import LayerDataDialog from './LayerDataDialog';
+import DeleteConfirmDialog from './DeleteConfirmDialog';
+import UsageGuide from './UsageGuide';
+
+interface SoilDataContentProps {
+ state: SoilDataState;
+ dispatch: React.Dispatch;
+}
+
+export default function SoilDataContent({ state, dispatch }: SoilDataContentProps) {
+ const handleExportData = () => {
+ // 生成CSV数据
+ const headers = ['采样点编号', '地块', '纬度', '经度', '采样日期', '采样人', '深度', 'pH值', '有机质(g/kg)', '全氮(g/kg)', '有效磷(mg/kg)', '速效钾(mg/kg)', '含水量(%)'];
+ const rows = state.samplePoints.flatMap(point =>
+ point.layers.map(layer => [
+ point.code,
+ point.fieldName,
+ point.latitude,
+ point.longitude,
+ point.sampleDate,
+ point.sampler,
+ layer.depth,
+ layer.pH,
+ layer.organicMatter,
+ layer.nitrogen,
+ layer.phosphorus,
+ layer.potassium,
+ layer.moisture,
+ ])
+ );
+
+ const csvContent = [
+ headers.join(','),
+ ...rows.map(row => row.join(',')),
+ ].join('\n');
+
+ // 创建下载链接
+ const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+ link.setAttribute('href', url);
+ link.setAttribute('download', `土壤基础数据_${new Date().toLocaleDateString()}.csv`);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ const handleGenerateReport = () => {
+ // 生成报告HTML内容
+ const reportHTML = `
+
+
+
+
+ 土壤检测报告
+
+
+
+ 土壤基础数据检测报告
+
+
报告概要
+
生成日期:${new Date().toLocaleDateString()}
+
采样点总数:${state.samplePoints.length} 个
+
覆盖地块:${state.statistics?.totalFields || 0} 个
+
分层样本:${state.statistics?.totalLayers || 0} 层
+
+
+ 采样点详细数据
+ ${state.samplePoints.map(point => `
+ ${point.code} - ${point.fieldName}
+ 坐标:${point.latitude.toFixed(6)}, ${point.longitude.toFixed(6)}
+ 采样日期:${point.sampleDate} | 采样人:${point.sampler}
+
+
+
+ | 深度 |
+ pH值 |
+ 有机质(g/kg) |
+ 全氮(g/kg) |
+ 有效磷(mg/kg) |
+ 速效钾(mg/kg) |
+ 含水量(%) |
+
+
+
+ ${point.layers.map(layer => `
+
+ | ${layer.depth} |
+ ${layer.pH} |
+ ${layer.organicMatter} |
+ ${layer.nitrogen} |
+ ${layer.phosphorus} |
+ ${layer.potassium} |
+ ${layer.moisture} |
+
+ `).join('')}
+
+
+ `).join('')}
+
+
+
+
+ `;
+
+ // 创建下载链接
+ const blob = new Blob([reportHTML], { type: 'text/html;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+ link.setAttribute('href', url);
+ link.setAttribute('download', `土壤检测报告_${new Date().toLocaleDateString()}.html`);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ return (
+
+ {/* 操作按钮区域 */}
+
+
+
+ 土壤采样点数据管理
+
+
+ 管理土壤采样点、记录分层数据、分析土壤理化性状
+
+
+
+
+
+
+
+
+
+ {/* 统计卡片 */}
+
+
+ {/* 主要内容区域 */}
+
dispatch({ type: 'SET_ACTIVE_TAB', payload: value })}>
+
+ 采样点列表
+ 空间分布
+ 剖面信息
+ 统计分析
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 对话框组件 */}
+
+
+
+
+
+ {/* 使用说明 */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SpatialDistribution.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SpatialDistribution.tsx
new file mode 100644
index 0000000..efa44dd
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/SpatialDistribution.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { SamplePoint } from './soilDataReducer';
+
+interface SpatialDistributionProps {
+ samplePoints: SamplePoint[];
+}
+
+export default function SpatialDistribution({ samplePoints }: SpatialDistributionProps) {
+ // 计算地图中心点
+ const getCenterPoint = () => {
+ if (samplePoints.length === 0) {
+ return { lat: 39.9042, lng: 116.4074 }; // 默认北京天安门
+ }
+
+ const avgLat = samplePoints.reduce((sum, p) => sum + p.latitude, 0) / samplePoints.length;
+ const avgLng = samplePoints.reduce((sum, p) => sum + p.longitude, 0) / samplePoints.length;
+
+ return { lat: avgLat, lng: avgLng };
+ };
+
+ const center = getCenterPoint();
+
+ return (
+
+
+ 采样点空间分布图
+
+ {/* 地图容器 - 暂时用占位符替代真实地图 */}
+
+ {/* 地图占位符 */}
+
+
+
+
地图组件加载中...
+
中心坐标: {center.lat.toFixed(4)}, {center.lng.toFixed(4)}
+
+
+
+ {/* 模拟采样点标记 */}
+ {samplePoints.map((point, index) => {
+ const relativeLat = ((point.latitude - center.lat) * 10000 + 50) % 100;
+ const relativeLng = ((point.longitude - center.lng) * 10000 + 50) % 100;
+
+ return (
+
+ );
+ })}
+
+ {/* 图例 */}
+
+
+
+
+ {/* 空间分布统计 */}
+
+
+ 采样密度统计
+
+ {Array.from(new Set(samplePoints.map(p => p.fieldName))).map(fieldName => {
+ const fieldPoints = samplePoints.filter(p => p.fieldName === fieldName);
+ return (
+
+ {fieldName}:
+ {fieldPoints.length} 个点
+
+ );
+ })}
+
+
+
+
+ pH值分布
+
+ {[
+ { label: '酸性', color: 'bg-orange-500', count: samplePoints.filter(p => p.layers[0]?.pH < 6.5).length },
+ { label: '中性', color: 'bg-green-500', count: samplePoints.filter(p => p.layers[0]?.pH >= 6.5 && p.layers[0]?.pH < 7.5).length },
+ { label: '碱性', color: 'bg-blue-500', count: samplePoints.filter(p => p.layers[0]?.pH >= 7.5).length },
+ ].map(item => (
+
+ ))}
+
+
+
+
+ 覆盖范围
+
+
+ 总采样点:
+ {samplePoints.length} 个
+
+
+ 覆盖地块:
+ {new Set(samplePoints.map(p => p.fieldName)).size} 个
+
+
+ 分层数据:
+ {samplePoints.reduce((sum, p) => sum + p.layers.length, 0)} 层
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/StatisticalAnalysis.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/StatisticalAnalysis.tsx
new file mode 100644
index 0000000..feb7ad5
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/StatisticalAnalysis.tsx
@@ -0,0 +1,246 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { SoilDataStatistics } from './soilDataReducer';
+
+interface StatisticalAnalysisProps {
+ statistics: SoilDataStatistics | null;
+}
+
+export default function StatisticalAnalysis({ statistics }: StatisticalAnalysisProps) {
+ if (!statistics) {
+ return (
+
+
+
+ );
+ }
+
+ const totalSamples = statistics.phDistribution.strongAcidic +
+ statistics.phDistribution.acidic +
+ statistics.phDistribution.neutral +
+ statistics.phDistribution.alkaline +
+ statistics.phDistribution.strongAlkaline;
+
+ const calculatePercentage = (value: number) => {
+ return totalSamples > 0 ? ((value / totalSamples) * 100).toFixed(0) : '0';
+ };
+
+ return (
+
+
+ {/* pH值分布统计 */}
+
+ pH值分布统计
+
+
+
+ 强酸性 (<5.5)
+ {statistics.phDistribution.strongAcidic} 个 ({calculatePercentage(statistics.phDistribution.strongAcidic)}%)
+
+
+
+
+
+ 酸性 (5.5-6.5)
+ {statistics.phDistribution.acidic} 个 ({calculatePercentage(statistics.phDistribution.acidic)}%)
+
+
+
+
+
+ 中性 (6.5-7.5)
+ {statistics.phDistribution.neutral} 个 ({calculatePercentage(statistics.phDistribution.neutral)}%)
+
+
+
+
+
+ 碱性 (7.5-8.5)
+ {statistics.phDistribution.alkaline} 个 ({calculatePercentage(statistics.phDistribution.alkaline)}%)
+
+
+
+
+
+ 强碱性 (>8.5)
+ {statistics.phDistribution.strongAlkaline} 个 ({calculatePercentage(statistics.phDistribution.strongAlkaline)}%)
+
+
+
+
+
+ {/* pH值评估 */}
+
+
+ pH值评估:
+ {statistics.phDistribution.neutral > totalSamples * 0.6 ?
+ ' 土壤酸碱度适中,适宜大多数作物生长' :
+ statistics.phDistribution.acidic > totalSamples * 0.5 ?
+ ' 土壤偏酸性,建议适量施用石灰调节' :
+ statistics.phDistribution.alkaline > totalSamples * 0.5 ?
+ ' 土壤偏碱性,建议适量施用酸性肥料调节' :
+ ' 土壤酸碱度需要进一步检测和调节'
+ }
+
+
+
+
+ {/* 有机质含量统计 */}
+
+ 有机质含量统计
+
+
+
+ 极低 (<10 g/kg)
+ {statistics.organicMatterDistribution.veryLow} 个 ({calculatePercentage(statistics.organicMatterDistribution.veryLow)}%)
+
+
+
+
+
+ 低 (10-20 g/kg)
+ {statistics.organicMatterDistribution.low} 个 ({calculatePercentage(statistics.organicMatterDistribution.low)}%)
+
+
+
+
+
+ 中等 (20-30 g/kg)
+ {statistics.organicMatterDistribution.medium} 个 ({calculatePercentage(statistics.organicMatterDistribution.medium)}%)
+
+
+
+
+
+ 较高 (30-40 g/kg)
+ {statistics.organicMatterDistribution.high} 个 ({calculatePercentage(statistics.organicMatterDistribution.high)}%)
+
+
+
+
+
+ 高 (>40 g/kg)
+ {statistics.organicMatterDistribution.veryHigh} 个 ({calculatePercentage(statistics.organicMatterDistribution.veryHigh)}%)
+
+
+
+
+
+ {/* 有机质评估 */}
+
+
+ 有机质评估:
+ {(statistics.organicMatterDistribution.high + statistics.organicMatterDistribution.veryHigh) > totalSamples * 0.5 ?
+ ' 土壤有机质含量丰富,肥力良好' :
+ (statistics.organicMatterDistribution.medium) > totalSamples * 0.5 ?
+ ' 土壤有机质含量中等,需要适量补充' :
+ ' 土壤有机质含量偏低,建议增施有机肥'
+ }
+
+
+
+
+
+ {/* 综合统计表格 */}
+
+ 数据统计概览
+
+
+
+ {statistics.totalPoints}
+
+
采样点总数
+
+
+
+ {statistics.totalFields}
+
+
覆盖地块数
+
+
+
+ {statistics.totalLayers}
+
+
分层样本数
+
+
+
+ {statistics.averagePH.toFixed(1)}
+
+
平均pH值
+
+
+
+ {/* 综合评估 */}
+
+
综合评估建议
+
+
• 数据覆盖度:已覆盖 {statistics.totalFields} 个地块,采样密度{statistics.totalPoints > 10 ? '充足' : '适中'}
+
• 土壤状况:平均pH值{statistics.averagePH.toFixed(1)},整体呈{statistics.averagePH < 6.5 ? '酸性' : statistics.averagePH > 7.5 ? '碱性' : '中性'}反应
+
• 改进建议:
+ {statistics.phDistribution.neutral > totalSamples * 0.5 ?
+ ' 土壤酸碱度状况良好,继续保持当前管理方式' :
+ ' 建议进行土壤改良,调节pH值至适宜范围'
+ }
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/StatisticsCards.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/StatisticsCards.tsx
new file mode 100644
index 0000000..12484a9
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/StatisticsCards.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { MapPin, Layers, BarChart3, Droplets } from 'lucide-react';
+import { SoilDataStatistics } from './soilDataReducer';
+
+interface StatisticsCardsProps {
+ statistics: SoilDataStatistics | null;
+}
+
+export default function StatisticsCards({ statistics }: StatisticsCardsProps) {
+ if (!statistics) {
+ return (
+
+ {[1, 2, 3, 4].map((i) => (
+
+
+
+ ))}
+
+ );
+ }
+
+ return (
+
+
+
+
+
采样点总数
+
+ {statistics.totalPoints}
+
+
+
+
+
+
+
+
+
+
覆盖地块
+
+ {statistics.totalFields} 个
+
+
+
+
+
+
+
+
+
+
分层样本
+
+ {statistics.totalLayers} 层
+
+
+
+
+
+
+
+
+
+
平均pH值
+
+ {statistics.averagePH.toFixed(1)}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/UsageGuide.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/UsageGuide.tsx
new file mode 100644
index 0000000..47a773a
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/UsageGuide.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import { Card } from '@/components/ui/card';
+import { AlertCircle } from 'lucide-react';
+
+export default function UsageGuide() {
+ return (
+
+
+
+
+
土壤基础数据管理说明:
+
+ - • 采样点管理: 记录GPS坐标、采样日期、负责人等基础信息
+ - • 分层采样: 支持多层次土壤数据(0-20cm、20-40cm、40-60cm等)
+ - • 理化指标: 记录pH值、有机质、氮磷钾、含水量等关键指标
+ - • 空间可视化: 在地图上显示采样点分布,分析覆盖密度
+ - • 剖面分析: 可视化展示土壤垂直分布特征
+ - • 统计分析: 自动计算平均值、标准差、变异系数等统计指标
+ - • 数据导出: 支持导出CSV格式,生成专业检测报告
+ - • IoT集成: 支持绑定物联网设备,自动读取土壤监测数据
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/components/soilDataReducer.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/soilDataReducer.tsx
new file mode 100644
index 0000000..9057d86
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/components/soilDataReducer.tsx
@@ -0,0 +1,500 @@
+'use client';
+
+import { toast } from 'sonner';
+
+// 土壤采样点接口
+export interface SoilLayer {
+ depth: string; // 如 "0-20cm"
+ deviceId?: string; // IoT设备ID,用于读取该层数据
+ // 以下字段从设备读取,仅用于展示
+ pH?: number;
+ organicMatter?: number; // 有机质 g/kg
+ nitrogen?: number; // 全氮 g/kg
+ phosphorus?: number; // 有效磷 mg/kg
+ potassium?: number; // 速效钾 mg/kg
+ moisture?: number; // 含水量 %
+}
+
+export interface SamplePoint {
+ id: string;
+ code: string;
+ fieldName: string;
+ latitude: number;
+ longitude: number;
+ sampleDate: string;
+ sampler: string;
+ deviceId?: string; // IoT设备ID
+ layers: SoilLayer[];
+}
+
+// IoT设备接口
+export interface IoTDevice {
+ id: string;
+ code: string;
+ name: string;
+ type: string;
+ status: 'online' | 'offline';
+ // 实时监测数据
+ data: {
+ pH: number;
+ organicMatter: number;
+ nitrogen: number;
+ phosphorus: number;
+ potassium: number;
+ moisture: number;
+ lastUpdate: string;
+ };
+}
+
+// 筛选条件接口
+export interface SoilDataFilters {
+ searchKeyword: string;
+ selectedField: string;
+ dateRange: {
+ start: string;
+ end: string;
+ };
+}
+
+// 统计结果接口
+export interface SoilDataStatistics {
+ totalPoints: number;
+ totalFields: number;
+ totalLayers: number;
+ averagePH: number;
+ phDistribution: {
+ strongAcidic: number; // 强酸性
+ acidic: number; // 酸性
+ neutral: number; // 中性
+ alkaline: number; // 碱性
+ strongAlkaline: number; // 强碱性
+ };
+ organicMatterDistribution: {
+ veryLow: number; // 极低
+ low: number; // 低
+ medium: number; // 中等
+ high: number; // 较高
+ veryHigh: number; // 高
+ };
+}
+
+// 状态接口
+export interface SoilDataState {
+ samplePoints: SamplePoint[];
+ iotDevices: IoTDevice[];
+ filters: SoilDataFilters;
+ statistics: SoilDataStatistics | null;
+ activeTab: string;
+ showAddDialog: boolean;
+ showEditDialog: boolean;
+ showLayerDialog: boolean;
+ showDeleteDialog: boolean;
+ selectedPoint: SamplePoint | null;
+ pointToDelete: string | null;
+ newPoint: Partial;
+ loading: boolean;
+}
+
+// Action类型
+export type SoilDataAction =
+ | { type: 'SET_SAMPLE_POINTS'; payload: SamplePoint[] }
+ | { type: 'SET_IOT_DEVICES'; payload: IoTDevice[] }
+ | { type: 'SET_FILTERS'; payload: Partial }
+ | { type: 'SET_STATISTICS'; payload: SoilDataStatistics | null }
+ | { type: 'SET_ACTIVE_TAB'; payload: string }
+ | { type: 'SET_SHOW_ADD_DIALOG'; payload: boolean }
+ | { type: 'SET_SHOW_EDIT_DIALOG'; payload: boolean }
+ | { type: 'SET_SHOW_LAYER_DIALOG'; payload: boolean }
+ | { type: 'SET_SHOW_DELETE_DIALOG'; payload: boolean }
+ | { type: 'SET_SELECTED_POINT'; payload: SamplePoint | null }
+ | { type: 'SET_POINT_TO_DELETE'; payload: string | null }
+ | { type: 'SET_NEW_POINT'; payload: Partial }
+ | { type: 'UPDATE_NEW_POINT'; payload: Partial }
+ | { type: 'RESET_NEW_POINT' }
+ | { type: 'SET_LOADING'; payload: boolean }
+ | { type: 'ADD_SAMPLE_POINT'; payload: SamplePoint }
+ | { type: 'UPDATE_SAMPLE_POINT'; payload: SamplePoint }
+ | { type: 'DELETE_SAMPLE_POINT'; payload: string }
+ | { type: 'LOAD_FROM_STORAGE' }
+ | { type: 'SAVE_TO_STORAGE' };
+
+// 初始状态
+export const initialSoilDataState: SoilDataState = {
+ samplePoints: [],
+ iotDevices: [],
+ filters: {
+ searchKeyword: '',
+ selectedField: 'all',
+ dateRange: {
+ start: '',
+ end: ''
+ }
+ },
+ statistics: null,
+ activeTab: 'list',
+ showAddDialog: false,
+ showEditDialog: false,
+ showLayerDialog: false,
+ showDeleteDialog: false,
+ selectedPoint: null,
+ pointToDelete: null,
+ newPoint: {
+ code: '',
+ fieldName: '',
+ latitude: 0,
+ longitude: 0,
+ sampleDate: '',
+ sampler: '',
+ deviceId: '',
+ layers: [
+ { depth: '0-20cm', deviceId: '' },
+ { depth: '20-40cm', deviceId: '' },
+ { depth: '40-60cm', deviceId: '' },
+ ]
+ },
+ loading: false
+};
+
+// 初始化测试数据
+const initializeTestData = () => {
+ const testDevices: IoTDevice[] = [
+ {
+ id: 'dev-1',
+ code: 'IOT-SOIL-001',
+ name: '土壤传感器001',
+ type: '多参数土壤传感器',
+ status: 'online',
+ data: {
+ pH: 6.8,
+ organicMatter: 28.5,
+ nitrogen: 1.2,
+ phosphorus: 25.3,
+ potassium: 180,
+ moisture: 22.5,
+ lastUpdate: '2024-10-18 14:30:00',
+ },
+ },
+ {
+ id: 'dev-2',
+ code: 'IOT-SOIL-002',
+ name: '土壤传感器002',
+ type: '多参数土壤传感器',
+ status: 'online',
+ data: {
+ pH: 7.2,
+ organicMatter: 32.1,
+ nitrogen: 1.5,
+ phosphorus: 28.6,
+ potassium: 195,
+ moisture: 20.3,
+ lastUpdate: '2024-10-18 14:28:00',
+ },
+ },
+ {
+ id: 'dev-3',
+ code: 'IOT-SOIL-003',
+ name: '土壤传感器003',
+ type: '多参数土壤传感器',
+ status: 'online',
+ data: {
+ pH: 5.8,
+ organicMatter: 22.3,
+ nitrogen: 0.9,
+ phosphorus: 18.5,
+ potassium: 152,
+ moisture: 24.6,
+ lastUpdate: '2024-10-18 14:25:00',
+ },
+ },
+ {
+ id: 'dev-4',
+ code: 'IOT-SOIL-004',
+ name: '土壤传感器004',
+ type: '多参数土壤传感器',
+ status: 'offline',
+ data: {
+ pH: 6.5,
+ organicMatter: 18.2,
+ nitrogen: 0.8,
+ phosphorus: 18.5,
+ potassium: 145,
+ moisture: 25.8,
+ lastUpdate: '2024-10-17 18:30:00',
+ },
+ },
+ ];
+
+ const testPoints: SamplePoint[] = [
+ {
+ id: 'sp-1',
+ code: 'SP001',
+ fieldName: '东区1号地',
+ latitude: 39.9042,
+ longitude: 116.4074,
+ sampleDate: '2024-10-15',
+ sampler: '张三',
+ deviceId: 'dev-1',
+ layers: [
+ { depth: '0-20cm', deviceId: 'dev-1', pH: 6.8, organicMatter: 28.5, nitrogen: 1.2, phosphorus: 25.3, potassium: 180, moisture: 22.5 },
+ { depth: '20-40cm', deviceId: 'dev-1', pH: 6.5, organicMatter: 18.2, nitrogen: 0.8, phosphorus: 18.5, potassium: 145, moisture: 25.8 },
+ { depth: '40-60cm', deviceId: 'dev-1', pH: 6.3, organicMatter: 12.5, nitrogen: 0.5, phosphorus: 12.3, potassium: 98, moisture: 28.2 },
+ ],
+ },
+ {
+ id: 'sp-2',
+ code: 'SP002',
+ fieldName: '东区1号地',
+ latitude: 39.9052,
+ longitude: 116.4084,
+ sampleDate: '2024-10-15',
+ sampler: '张三',
+ deviceId: 'dev-2',
+ layers: [
+ { depth: '0-20cm', deviceId: 'dev-2', pH: 7.2, organicMatter: 32.1, nitrogen: 1.5, phosphorus: 28.6, potassium: 195, moisture: 20.3 },
+ { depth: '20-40cm', deviceId: 'dev-2', pH: 6.8, organicMatter: 22.3, nitrogen: 1.0, phosphorus: 21.2, potassium: 158, moisture: 23.5 },
+ ],
+ },
+ {
+ id: 'sp-3',
+ code: 'SP003',
+ fieldName: '西区2号地',
+ latitude: 39.9032,
+ longitude: 116.4064,
+ sampleDate: '2024-10-14',
+ sampler: '李四',
+ deviceId: 'dev-3',
+ layers: [
+ { depth: '0-20cm', deviceId: 'dev-3', pH: 5.8, organicMatter: 22.3, nitrogen: 0.9, phosphorus: 18.5, potassium: 152, moisture: 24.6 },
+ { depth: '20-40cm', deviceId: 'dev-3', pH: 5.5, organicMatter: 15.8, nitrogen: 0.6, phosphorus: 14.2, potassium: 125, moisture: 26.8 },
+ { depth: '40-60cm', deviceId: 'dev-3', pH: 5.3, organicMatter: 10.2, nitrogen: 0.4, phosphorus: 9.8, potassium: 88, moisture: 29.1 },
+ ],
+ },
+ ];
+
+ return { devices: testDevices, points: testPoints };
+};
+
+// 计算统计数据
+const calculateStatistics = (points: SamplePoint[]): SoilDataStatistics => {
+ const totalPoints = points.length;
+ const uniqueFields = new Set(points.map(p => p.fieldName));
+ const totalFields = uniqueFields.size;
+ const totalLayers = points.reduce((sum, p) => sum + p.layers.length, 0);
+
+ // 计算平均pH值
+ const phValues = points.flatMap(p => p.layers.map(l => l.pH || 0));
+ const averagePH = phValues.length > 0 ? phValues.reduce((sum, ph) => sum + ph, 0) / phValues.length : 0;
+
+ // pH分布统计
+ const phDistribution = phValues.reduce((acc, ph) => {
+ if (ph < 5.5) acc.strongAcidic++;
+ else if (ph < 6.5) acc.acidic++;
+ else if (ph < 7.5) acc.neutral++;
+ else if (ph < 8.5) acc.alkaline++;
+ else acc.strongAlkaline++;
+ return acc;
+ }, { strongAcidic: 0, acidic: 0, neutral: 0, alkaline: 0, strongAlkaline: 0 });
+
+ // 有机质分布统计
+ const organicMatterValues = points.flatMap(p => p.layers.map(l => l.organicMatter || 0));
+ const organicMatterDistribution = organicMatterValues.reduce((acc, om) => {
+ if (om < 10) acc.veryLow++;
+ else if (om < 20) acc.low++;
+ else if (om < 30) acc.medium++;
+ else if (om < 40) acc.high++;
+ else acc.veryHigh++;
+ return acc;
+ }, { veryLow: 0, low: 0, medium: 0, high: 0, veryHigh: 0 });
+
+ return {
+ totalPoints,
+ totalFields,
+ totalLayers,
+ averagePH,
+ phDistribution,
+ organicMatterDistribution
+ };
+};
+
+// Reducer函数
+export function soilDataReducer(state: SoilDataState, action: SoilDataAction): SoilDataState {
+ switch (action.type) {
+ case 'SET_SAMPLE_POINTS':
+ return {
+ ...state,
+ samplePoints: action.payload,
+ statistics: calculateStatistics(action.payload)
+ };
+
+ case 'SET_IOT_DEVICES':
+ return { ...state, iotDevices: action.payload };
+
+ case 'SET_FILTERS':
+ return {
+ ...state,
+ filters: { ...state.filters, ...action.payload }
+ };
+
+ case 'SET_STATISTICS':
+ return { ...state, statistics: action.payload };
+
+ case 'SET_ACTIVE_TAB':
+ return { ...state, activeTab: action.payload };
+
+ case 'SET_SHOW_ADD_DIALOG':
+ if (action.payload) {
+ return {
+ ...state,
+ showAddDialog: action.payload,
+ newPoint: {
+ code: '',
+ fieldName: '',
+ latitude: 0,
+ longitude: 0,
+ sampleDate: '',
+ sampler: '',
+ deviceId: '',
+ layers: [
+ { depth: '0-20cm', deviceId: '' },
+ { depth: '20-40cm', deviceId: '' },
+ { depth: '40-60cm', deviceId: '' },
+ ]
+ }
+ };
+ }
+ return { ...state, showAddDialog: action.payload };
+
+ case 'SET_SHOW_EDIT_DIALOG':
+ return { ...state, showEditDialog: action.payload };
+
+ case 'SET_SHOW_LAYER_DIALOG':
+ return { ...state, showLayerDialog: action.payload };
+
+ case 'SET_SHOW_DELETE_DIALOG':
+ return { ...state, showDeleteDialog: action.payload };
+
+ case 'SET_SELECTED_POINT':
+ return { ...state, selectedPoint: action.payload };
+
+ case 'SET_POINT_TO_DELETE':
+ return { ...state, pointToDelete: action.payload };
+
+ case 'SET_NEW_POINT':
+ return { ...state, newPoint: action.payload };
+
+ case 'UPDATE_NEW_POINT':
+ return {
+ ...state,
+ newPoint: { ...state.newPoint, ...action.payload }
+ };
+
+ case 'RESET_NEW_POINT':
+ return {
+ ...state,
+ newPoint: {
+ code: '',
+ fieldName: '',
+ latitude: 0,
+ longitude: 0,
+ sampleDate: '',
+ sampler: '',
+ deviceId: '',
+ layers: [
+ { depth: '0-20cm', deviceId: '' },
+ { depth: '20-40cm', deviceId: '' },
+ { depth: '40-60cm', deviceId: '' },
+ ]
+ }
+ };
+
+ case 'SET_LOADING':
+ return { ...state, loading: action.payload };
+
+ case 'ADD_SAMPLE_POINT':
+ const updatedPoints = [...state.samplePoints, action.payload];
+ toast.success('采样点添加成功!');
+ return {
+ ...state,
+ samplePoints: updatedPoints,
+ statistics: calculateStatistics(updatedPoints)
+ };
+
+ case 'UPDATE_SAMPLE_POINT':
+ const updatedPointsList = state.samplePoints.map(point =>
+ point.id === action.payload.id ? action.payload : point
+ );
+ toast.success('采样点更新成功!');
+ return {
+ ...state,
+ samplePoints: updatedPointsList,
+ statistics: calculateStatistics(updatedPointsList)
+ };
+
+ case 'DELETE_SAMPLE_POINT':
+ const filteredPoints = state.samplePoints.filter(p => p.id !== action.payload);
+ toast.success('采样点已删除');
+ return {
+ ...state,
+ samplePoints: filteredPoints,
+ statistics: calculateStatistics(filteredPoints)
+ };
+
+ case 'LOAD_FROM_STORAGE':
+ try {
+ const stored = localStorage.getItem('soilData');
+ if (stored) {
+ const parsedData = JSON.parse(stored);
+ return {
+ ...state,
+ samplePoints: parsedData.samplePoints || [],
+ statistics: calculateStatistics(parsedData.samplePoints || [])
+ };
+ } else {
+ // 首次加载,初始化测试数据
+ const testData = initializeTestData();
+ localStorage.setItem('soilData', JSON.stringify({
+ samplePoints: testData.points
+ }));
+ return {
+ ...state,
+ samplePoints: testData.points,
+ iotDevices: testData.devices,
+ statistics: calculateStatistics(testData.points)
+ };
+ }
+ } catch (error) {
+ console.error('Failed to load soil data from storage:', error);
+ return state;
+ }
+
+ case 'SAVE_TO_STORAGE':
+ try {
+ localStorage.setItem('soilData', JSON.stringify({
+ samplePoints: state.samplePoints
+ }));
+ } catch (error) {
+ console.error('Failed to save soil data to storage:', error);
+ }
+ return state;
+
+ default:
+ return state;
+ }
+}
+
+// 工具函数
+export const getPHLevel = (pH: number) => {
+ if (pH < 5.5) return { label: '强酸性', color: 'bg-red-500' };
+ if (pH < 6.5) return { label: '酸性', color: 'bg-orange-500' };
+ if (pH < 7.5) return { label: '中性', color: 'bg-green-500' };
+ if (pH < 8.5) return { label: '碱性', color: 'bg-blue-500' };
+ return { label: '强碱性', color: 'bg-purple-500' };
+};
+
+export const getOrganicMatterLevel = (om: number) => {
+ if (om < 10) return { label: '极低', color: 'text-red-600' };
+ if (om < 20) return { label: '低', color: 'text-orange-600' };
+ if (om < 30) return { label: '中等', color: 'text-yellow-600' };
+ if (om < 40) return { label: '较高', color: 'text-green-600' };
+ return { label: '高', color: 'text-blue-600' };
+};
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/analysis/soil-data/page.tsx b/crop-x/src/app/(app)/land-information/analysis/soil-data/page.tsx
index 2e5d3f1..bde1f3e 100644
--- a/crop-x/src/app/(app)/land-information/analysis/soil-data/page.tsx
+++ b/crop-x/src/app/(app)/land-information/analysis/soil-data/page.tsx
@@ -1,18 +1,26 @@
'use client';
+import { useReducer, useEffect } from 'react';
import { Card } from '@/components/ui/card';
+import { soilDataReducer, initialSoilDataState, SoilDataState } from './components/soilDataReducer';
+import SoilDataContent from './components/SoilDataContent';
export default function SoilDataPage() {
+ const [state, dispatch] = useReducer(soilDataReducer, initialSoilDataState);
+
+ useEffect(() => {
+ // 加载存储的数据
+ dispatch({ type: 'LOAD_FROM_STORAGE' });
+ }, []);
+
+ // 保存数据到localStorage
+ useEffect(() => {
+ dispatch({ type: 'SAVE_TO_STORAGE' });
+ }, [state.samplePoints]);
+
return (
-
- 土壤基础数据
-
-
- 页面路径: /land-information/analysis/soil-data
-
-
-
+
);
}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/map/satellite/components/FieldSatellite.tsx b/crop-x/src/app/(app)/land-information/map/satellite/components/FieldSatellite.tsx
new file mode 100644
index 0000000..0492a88
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/map/satellite/components/FieldSatellite.tsx
@@ -0,0 +1,873 @@
+/**
+ * 地块影像组件
+ * 集成时序遥感影像服务,支持天地图、Sentinel、Landsat等数据源
+ */
+
+'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 { Slider } from '@/components/ui/slider';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ Satellite,
+ Calendar,
+ Image as ImageIcon,
+ TrendingUp,
+ TrendingDown,
+ Layers,
+ Download,
+ Eye,
+ BarChart3,
+ Cloud,
+ Leaf,
+ Sun,
+ Droplets,
+ AlertCircle,
+ RefreshCw,
+ Filter,
+ Maximize2,
+ ChevronLeft,
+ ChevronRight,
+ Activity
+} from 'lucide-react';
+import { toast } from 'sonner';
+import {
+ SatelliteImageService,
+ DATA_SOURCES,
+ getCloudCoverColorClass,
+ formatImageDate
+} from './satelliteImageService';
+import {
+ SatelliteImage,
+ ImageComparisonResult,
+ TimeSeriesAnalysis
+} from './satelliteTypes';
+
+export function FieldSatellite() {
+ const [selectedField, setSelectedField] = useState('field-1');
+ const [imageSource, setImageSource] = useState('Sentinel-2');
+ const [selectedImage, setSelectedImage] = useState(null);
+ const [comparisonImage, setComparisonImage] = useState(null);
+ const [showComparison, setShowComparison] = useState(false);
+ const [timeSliderValue, setTimeSliderValue] = useState([0]);
+ const [maxCloudCover, setMaxCloudCover] = useState(30);
+ const [images, setImages] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [comparisonResult, setComparisonResult] = useState(null);
+ const [timeSeriesAnalysis, setTimeSeriesAnalysis] = useState(null);
+ const [activeView, setActiveView] = useState<'single' | 'comparison' | 'timeseries'>('single');
+
+ // 模拟地块数据
+ const mockFields = [
+ { id: 'field-1', name: '东区1号地', code: 'DB001' },
+ { id: 'field-2', name: '西区2号地', code: 'DB002' },
+ { id: 'field-3', name: '南区3号地', code: 'DB003' },
+ ];
+
+ // 加载影像数据
+ useEffect(() => {
+ loadImages();
+ }, [selectedField, imageSource, maxCloudCover]);
+
+ // 自动选择第一张影像
+ useEffect(() => {
+ if (images.length > 0 && !selectedImage) {
+ setSelectedImage(images[0]);
+ setTimeSliderValue([0]);
+ }
+ }, [images]);
+
+ // 更新时序分析
+ useEffect(() => {
+ if (images.length >= 2) {
+ const analysis = SatelliteImageService.analyzeTimeSeries(images);
+ setTimeSeriesAnalysis(analysis);
+ }
+ }, [images]);
+
+ const loadImages = async () => {
+ setIsLoading(true);
+ try {
+ // 获取最近6个月的影像
+ const endDate = new Date();
+ const startDate = new Date();
+ startDate.setMonth(startDate.getMonth() - 6);
+
+ const loadedImages = await SatelliteImageService.getFieldImages(
+ selectedField,
+ startDate.toISOString().split('T')[0],
+ endDate.toISOString().split('T')[0],
+ imageSource,
+ maxCloudCover
+ );
+
+ setImages(loadedImages);
+ toast.success(`加载了 ${loadedImages.length} 张影像`);
+ } catch (error) {
+ toast.error('加载影像失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleImageSelect = (image: SatelliteImage, index: number) => {
+ setSelectedImage(image);
+ setTimeSliderValue([index]);
+ toast.success(`已加载 ${formatImageDate(image.date)} 影像`);
+ };
+
+ const handleTimeSliderChange = (value: number[]) => {
+ setTimeSliderValue(value);
+ if (images[value[0]]) {
+ setSelectedImage(images[value[0]]);
+ }
+ };
+
+ const handleComparisonToggle = () => {
+ if (!showComparison) {
+ // 开启对比模式,选择当前影像的前一张作为对比
+ const currentIndex = timeSliderValue[0];
+ if (currentIndex < images.length - 1) {
+ setComparisonImage(images[currentIndex + 1]);
+ } else if (images.length >= 2) {
+ setComparisonImage(images[0]);
+ }
+ }
+ setShowComparison(!showComparison);
+ setActiveView(showComparison ? 'single' : 'comparison');
+ };
+
+ const handleCompare = () => {
+ if (!selectedImage || !comparisonImage) {
+ toast.error('请选择两张影像进行对比');
+ return;
+ }
+
+ const result = SatelliteImageService.compareImages(comparisonImage, selectedImage);
+ setComparisonResult(result);
+ toast.success('影像对比完成');
+ };
+
+ const handleDownload = async () => {
+ if (!selectedImage) {
+ toast.error('请先选择影像');
+ return;
+ }
+
+ try {
+ await SatelliteImageService.downloadImage(selectedImage, 'jpg');
+ toast.success(`正在下载 ${formatImageDate(selectedImage.date)} 影像...`);
+ } catch (error) {
+ toast.error('下载失败');
+ }
+ };
+
+ const navigateImage = (direction: 'prev' | 'next') => {
+ const currentIndex = timeSliderValue[0];
+ let newIndex = currentIndex;
+
+ if (direction === 'prev' && currentIndex > 0) {
+ newIndex = currentIndex - 1;
+ } else if (direction === 'next' && currentIndex < images.length - 1) {
+ newIndex = currentIndex + 1;
+ }
+
+ if (newIndex !== currentIndex) {
+ handleImageSelect(images[newIndex], newIndex);
+ }
+ };
+
+ return (
+
+
+
+
地块影像
+
+ 时序遥感影像分析与作物长势监测
+
+
+
+
+
+
+
+
+
+ {/* 数据源和地块信息 */}
+
+
+
+
+
+
数据源
+
{DATA_SOURCES[imageSource as keyof typeof DATA_SOURCES]?.name || imageSource}
+
+
+
+
+
+
+
+
影像数量
+
{images.length} 张
+
+
+
+
+
+
+
+
分辨率
+
{DATA_SOURCES[imageSource as keyof typeof DATA_SOURCES]?.resolution || '--'}米
+
+
+
+
+
+
+
+
健康分数
+
{timeSeriesAnalysis?.healthScore || '--'}
+
+
+
+
+
+
+ {/* 左侧控制面板 */}
+
+ {/* 地块选择 */}
+
+
+
+
+
+ {/* 数据源选择 */}
+
+
+
+ {Object.entries(DATA_SOURCES).map(([key, source]) => (
+
+ ))}
+
+
+
+ {/* 云量过滤 */}
+
+
+ setMaxCloudCover(value[0])}
+ max={100}
+ step={5}
+ className="w-full"
+ />
+
+
+ 仅显示云量 ≤ {maxCloudCover}% 的影像
+
+
+
+ {/* 视图切换 */}
+
+
+
+
+
+
+
+
+
+ {/* 指标说明 */}
+
+ 植被指数说明
+
+
+
+
+ NDVI
+
+
+ 归一化植被指数,反映植被覆盖度和长势
+
+
+
+
+
+ EVI
+
+
+ 增强型植被指数,对高生物量更敏感
+
+
+
+
+
+ 土壤调节植被指数,减少土壤背景影响
+
+
+
+
+
+
+ {/* 主显示区域 */}
+
+
setActiveView(v as any)}>
+
+ 单影像
+ 影像对比
+ 时序分析
+
+
+ {/* 单影像视图 */}
+
+ {/* 影像显示 */}
+
+
+ {/* 模拟卫星影像 */}
+
+
+ {/* 导航按钮 */}
+
+
+
+
+
+ {/* 影像信息叠加 */}
+ {selectedImage && (
+ <>
+
+
+
+
+
+ {formatImageDate(selectedImage.date)}
+ {selectedImage.season}
+
+
+
+
+
+ 云量: {selectedImage.cloudCover.toFixed(0)}%
+
+
+
+
+
+ {/* NDVI图例 */}
+
+
+ {/* 当前NDVI值 */}
+
+
+
+
当前NDVI
+
+ {selectedImage.ndvi.toFixed(2)}
+
+
+ {SatelliteImageService.getNDVILabel(selectedImage.ndvi)}
+
+
+
+
+ >
+ )}
+
+
+
+ {/* 作物长势分析 */}
+ {selectedImage && (
+
+ 作物长势分析
+
+
+
+
+ 植被覆盖度
+
+
+ {(selectedImage.ndvi * 100).toFixed(0)}%
+
+
+
+
+
+ 长势评价
+
+
+ {SatelliteImageService.getNDVILabel(selectedImage.ndvi)}
+
+
+
+
+
+ 叶面积指数
+
+ {selectedImage.lai.toFixed(1)}
+
+
+
+
+ 增强植被指数
+
+ {selectedImage.evi.toFixed(2)}
+
+
+
+ )}
+
+
+ {/* 影像对比视图 */}
+
+
+
+
影像对比分析
+
+
+
+ {/* 选择对比影像 */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* 对比结果 */}
+ {comparisonResult && (
+
+
+
+ {comparisonResult.changeType === 'improvement' &&
}
+ {comparisonResult.changeType === 'decline' &&
}
+ {comparisonResult.changeType === 'stable' &&
}
+
+
+ {comparisonResult.changeDescription}
+
+
+
+
+
+
+
+ NDVI变化
+
+ 0 ? '#22c55e' :
+ comparisonResult.ndviChange < 0 ? '#ef4444' : '#6b7280'
+ }}>
+ {comparisonResult.ndviChange > 0 ? '+' : ''}{comparisonResult.ndviChange.toFixed(3)}
+
+
+ ({comparisonResult.image1.ndvi.toFixed(2)} → {comparisonResult.image2.ndvi.toFixed(2)})
+
+
+
+
+ EVI变化
+
+ 0 ? '#22c55e' :
+ comparisonResult.eviChange < 0 ? '#ef4444' : '#6b7280'
+ }}>
+ {comparisonResult.eviChange > 0 ? '+' : ''}{comparisonResult.eviChange.toFixed(3)}
+
+
+ ({comparisonResult.image1.evi.toFixed(2)} → {comparisonResult.image2.evi.toFixed(2)})
+
+
+
+
+
+
+ 管理建议
+
+ {comparisonResult.recommendations.map((rec, index) => (
+ -
+ •
+ {rec}
+
+ ))}
+
+
+
+ )}
+
+
+
+ {/* 时序分析视图 */}
+
+ {timeSeriesAnalysis && (
+ <>
+
+ 生长趋势分析
+
+
+ 变化趋势
+
+ {timeSeriesAnalysis.trend === 'increasing' &&
}
+ {timeSeriesAnalysis.trend === 'decreasing' &&
}
+ {timeSeriesAnalysis.trend === 'stable' &&
}
+ {timeSeriesAnalysis.trend === 'fluctuating' &&
}
+
+ {timeSeriesAnalysis.trend === 'increasing' && '上升'}
+ {timeSeriesAnalysis.trend === 'decreasing' && '下降'}
+ {timeSeriesAnalysis.trend === 'stable' && '稳定'}
+ {timeSeriesAnalysis.trend === 'fluctuating' && '波动'}
+
+
+
+
+ 生长阶段
+ {timeSeriesAnalysis.growthStage}
+
+
+ 健康分数
+ {timeSeriesAnalysis.healthScore}
+
+
+ 影像数量
+ {timeSeriesAnalysis.dates.length}
+
+
+
+ {/* NDVI趋势图 */}
+
+
NDVI变化曲线
+
+
+ {/* 刻度标签 */}
+
+ {timeSeriesAnalysis.dates[0]}
+ {timeSeriesAnalysis.dates[timeSeriesAnalysis.dates.length - 1]}
+
+
+
+
+ {/* 警报信息 */}
+ {timeSeriesAnalysis.alerts.length > 0 && (
+
+
+
+ 注意事项
+
+
+ {timeSeriesAnalysis.alerts.map((alert, index) => (
+ -
+ {alert}
+
+ ))}
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* 时间滑块(所有视图通用) */}
+ {images.length > 0 && (
+
+
+
+
+
+ 时间轴
+
+ {images[timeSliderValue[0]] && formatImageDate(images[timeSliderValue[0]].date)}
+
+
+
+
+ {images[0] && formatImageDate(images[0].date)}
+ {images[images.length - 1] && formatImageDate(images[images.length - 1].date)}
+
+
+
+
+ )}
+
+ {/* 影像列表 */}
+
+ 历史影像列表
+
+ {images.map((image, index) => (
+
handleImageSelect(image, index)}
+ >
+
+
+
+ {formatImageDate(image.date)}
+
+
+
+ {image.cloudCover.toFixed(0)}%
+
+
+
+
+ 数据源:
+ {image.source}
+
+
+ 分辨率:
+ {image.resolution}m
+
+
+ NDVI:
+
+ {image.ndvi.toFixed(2)}
+
+
+
+ EVI:
+ {image.evi.toFixed(2)}
+
+
+
+ ))}
+
+
+
+
+
+ {/* 使用说明 */}
+
+
+
+
+
遥感影像功能说明:
+
+ - • 时序影像:通过时间滑块查看同一地块不同时期的卫星影像,直观对比地块变化
+ - • 影像对比:选择两个时期的影像进行对比,自动计算NDVI、EVI变化并生成管理建议
+ - • 时序分析:分析NDVI变化趋势,判断作物生长阶段,计算健康分数
+ - • 多数据源:支持Sentinel-2(10米)、Landsat-8(30米)、天地图等多种数据源
+ - • 云量过滤:可设置最大云量阈值,自动过滤云量过高的影像,确保分析准确性
+ - • 植被指数:NDVI(植被覆盖度)、EVI(高生物量敏感)、SAVI(土壤调节)、LAI(叶面积指数)
+ - • 智能建议:根据影像变化自动生成灌溉、施肥、病虫害防治等管理建议
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/map/satellite/components/satelliteImageService.ts b/crop-x/src/app/(app)/land-information/map/satellite/components/satelliteImageService.ts
new file mode 100644
index 0000000..cddd494
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/map/satellite/components/satelliteImageService.ts
@@ -0,0 +1,352 @@
+/**
+ * 卫星影像服务类
+ * 提供卫星影像数据的获取、分析和对比功能
+ */
+
+import { SatelliteImage, ImageComparisonResult, TimeSeriesAnalysis, DataSource } from './satelliteTypes';
+
+export class SatelliteImageService {
+ /**
+ * 获取指定地块的卫星影像数据
+ */
+ static async getFieldImages(
+ fieldId: string,
+ startDate: string,
+ endDate: string,
+ source: string = 'Sentinel-2',
+ maxCloudCover: number = 30
+ ): Promise {
+ // 模拟API调用,生成测试数据
+ const mockImages: SatelliteImage[] = [];
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ // 生成6个月的影像数据,每15天一张
+ const intervalDays = 15;
+ const totalImages = Math.floor((end.getTime() - start.getTime()) / (intervalDays * 24 * 60 * 60 * 1000));
+
+ for (let i = 0; i < totalImages; i++) {
+ const date = new Date(start);
+ date.setDate(date.getDate() + i * intervalDays);
+
+ // 模拟NDVI变化趋势(作物生长周期)
+ const dayOfYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / (24 * 60 * 60 * 1000));
+ const ndviBase = 0.3 + 0.4 * Math.sin((dayOfYear - 90) * Math.PI / 180);
+ const ndvi = Math.max(0.1, Math.min(0.9, ndviBase + (Math.random() - 0.5) * 0.2));
+
+ // 相关植被指数计算
+ const evi = ndvi * (0.9 + Math.random() * 0.2);
+ const lai = ndvi * 6 + Math.random() * 2;
+
+ // 随机云量
+ const cloudCover = Math.random() * 50;
+
+ // 只包含云量符合要求的影像
+ if (cloudCover <= maxCloudCover) {
+ mockImages.push({
+ id: `${fieldId}-${source}-${date.toISOString().split('T')[0]}`,
+ date: date.toISOString().split('T')[0],
+ source,
+ resolution: parseInt(DATA_SOURCES[source]?.resolution || '10'),
+ cloudCover,
+ ndvi,
+ evi,
+ lai,
+ season: SatelliteImageService.getSeason(date),
+ thumbnail: SatelliteImageService.generateThumbnailUrl(date, source),
+ url: SatelliteImageService.generateImageUrl(date, source)
+ });
+ }
+ }
+
+ return mockImages.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+ }
+
+ /**
+ * 对比两张影像
+ */
+ static compareImages(image1: SatelliteImage, image2: SatelliteImage): ImageComparisonResult {
+ const ndviChange = image2.ndvi - image1.ndvi;
+ const eviChange = image2.evi - image1.evi;
+
+ let changeType: 'improvement' | 'decline' | 'stable';
+ let changeDescription: string;
+
+ const threshold = 0.05; // NDVI变化阈值
+
+ if (ndviChange > threshold) {
+ changeType = 'improvement';
+ changeDescription = '作物长势明显改善';
+ } else if (ndviChange < -threshold) {
+ changeType = 'decline';
+ changeDescription = '作物长势出现下降';
+ } else {
+ changeType = 'stable';
+ changeDescription = '作物长势保持稳定';
+ }
+
+ // 生成管理建议
+ const recommendations = this.generateRecommendations(changeType, ndviChange, eviChange);
+
+ return {
+ image1,
+ image2,
+ ndviChange,
+ eviChange,
+ changeType,
+ changeDescription,
+ recommendations
+ };
+ }
+
+ /**
+ * 分析时序数据
+ */
+ static analyzeTimeSeries(images: SatelliteImage[]): TimeSeriesAnalysis {
+ if (images.length === 0) {
+ return {
+ dates: [],
+ ndviValues: [],
+ trend: 'stable',
+ growthStage: '无数据',
+ healthScore: '--',
+ alerts: []
+ };
+ }
+
+ // 按日期排序
+ const sortedImages = images.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
+
+ const dates = sortedImages.map(img => img.date);
+ const ndviValues = sortedImages.map(img => img.ndvi);
+
+ // 分析趋势
+ const trend = this.analyzeTrend(ndviValues);
+
+ // 判断生长阶段
+ const growthStage = this.determineGrowthStage(ndviValues);
+
+ // 计算健康分数
+ const healthScore = this.calculateHealthScore(ndviValues);
+
+ // 生成警报
+ const alerts = this.generateAlerts(ndviValues, sortedImages);
+
+ return {
+ dates,
+ ndviValues,
+ trend,
+ growthStage,
+ healthScore,
+ alerts
+ };
+ }
+
+ /**
+ * 下载影像
+ */
+ static async downloadImage(image: SatelliteImage, format: 'jpg' | 'png' | 'tiff' = 'jpg'): Promise {
+ // 模拟下载过程
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ // 模拟下载成功
+ console.log(`Downloading ${image.url} as ${format}`);
+ resolve();
+ }, 2000);
+ });
+ }
+
+ /**
+ * 获取NDVI对应的颜色
+ */
+ static getNDVIColor(ndvi: number): string {
+ if (ndvi < 0.2) return '#ef4444'; // 红色
+ if (ndvi < 0.4) return '#f97316'; // 橙色
+ if (ndvi < 0.6) return '#eab308'; // 黄色
+ if (ndvi < 0.8) return '#84cc16'; // 浅绿色
+ return '#22c55e'; // 深绿色
+ }
+
+ /**
+ * 获取NDVI对应的描述
+ */
+ static getNDVILabel(ndvi: number): string {
+ if (ndvi < 0.2) return '植被稀疏';
+ if (ndvi < 0.4) return '植被较少';
+ if (ndvi < 0.6) return '植被适中';
+ if (ndvi < 0.8) return '植被良好';
+ return '植被茂盛';
+ }
+
+ // 私有方法
+
+ private static getSeason(date: Date): string {
+ const month = date.getMonth();
+ if (month >= 2 && month <= 4) return '春季';
+ if (month >= 5 && month <= 7) return '夏季';
+ if (month >= 8 && month <= 10) return '秋季';
+ return '冬季';
+ }
+
+ private static generateThumbnailUrl(date: Date, source: string): string {
+ const dateStr = date.toISOString().split('T')[0];
+ return `https://picsum.photos/seed/${source}-${dateStr}/200/200.jpg`;
+ }
+
+ private static generateImageUrl(date: Date, source: string): string {
+ const dateStr = date.toISOString().split('T')[0];
+ return `https://picsum.photos/seed/${source}-${dateStr}/800/600.jpg`;
+ }
+
+ private static analyzeTrend(values: number[]): 'increasing' | 'decreasing' | 'stable' | 'fluctuating' {
+ if (values.length < 2) return 'stable';
+
+ let increaseCount = 0;
+ let decreaseCount = 0;
+
+ for (let i = 1; i < values.length; i++) {
+ if (values[i] > values[i - 1] + 0.02) increaseCount++;
+ else if (values[i] < values[i - 1] - 0.02) decreaseCount++;
+ }
+
+ const totalChanges = values.length - 1;
+ const increaseRatio = increaseCount / totalChanges;
+ const decreaseRatio = decreaseCount / totalChanges;
+
+ if (increaseRatio > 0.6) return 'increasing';
+ if (decreaseRatio > 0.6) return 'decreasing';
+ if (increaseRatio > 0.3 && decreaseRatio > 0.3) return 'fluctuating';
+ return 'stable';
+ }
+
+ private static determineGrowthStage(ndviValues: number[]): string {
+ if (ndviValues.length === 0) return '未知';
+
+ const avgNdvi = ndviValues.reduce((sum, val) => sum + val, 0) / ndviValues.length;
+ const latestNdvi = ndviValues[ndviValues.length - 1];
+
+ if (avgNdvi < 0.3) return '苗期';
+ if (avgNdvi < 0.5) return '生长期';
+ if (avgNdvi < 0.7) return '旺盛期';
+ return '成熟期';
+ }
+
+ private static calculateHealthScore(ndviValues: number[]): string {
+ if (ndviValues.length === 0) return '--';
+
+ const avgNdvi = ndviValues.reduce((sum, val) => sum + val, 0) / ndviValues.length;
+ const latestNdvi = ndviValues[ndviValues.length - 1];
+
+ // 综合平均NDVI和最新NDVI计算健康分数
+ const healthScore = Math.round((avgNdvi * 0.6 + latestNdvi * 0.4) * 100);
+ return `${healthScore}/100`;
+ }
+
+ private static generateAlerts(ndviValues: number[], images: SatelliteImage[]): string[] {
+ const alerts: string[] = [];
+
+ if (ndviValues.length < 2) return alerts;
+
+ const latestNdvi = ndviValues[ndviValues.length - 1];
+ const previousNdvi = ndviValues[ndviValues.length - 2];
+ const ndviChange = latestNdvi - previousNdvi;
+
+ // NDVI下降警报
+ if (ndviChange < -0.1) {
+ alerts.push('NDVI显著下降,建议检查灌溉和病虫害情况');
+ }
+
+ // NDVI过低警报
+ if (latestNdvi < 0.3) {
+ alerts.push('植被覆盖度过低,建议加强田间管理');
+ }
+
+ // 云量过高警报
+ const highCloudImages = images.filter(img => img.cloudCover > 40);
+ if (highCloudImages.length > images.length * 0.5) {
+ alerts.push('近期影像云量较高,可能影响分析准确性');
+ }
+
+ return alerts;
+ }
+
+ private static generateRecommendations(
+ changeType: 'improvement' | 'decline' | 'stable',
+ ndviChange: number,
+ eviChange: number
+ ): string[] {
+ const recommendations: string[] = [];
+
+ if (changeType === 'improvement') {
+ recommendations.push('作物长势良好,继续保持当前管理措施');
+ recommendations.push('可适当增加施肥量,维持作物生长势头');
+ if (Math.abs(eviChange) > 0.05) {
+ recommendations.push('EVI指数变化明显,建议监测叶面积指数变化');
+ }
+ } else if (changeType === 'decline') {
+ recommendations.push('作物长势下降,建议立即检查灌溉情况');
+ recommendations.push('加强病虫害监测,必要时采取防治措施');
+ recommendations.push('考虑补充施肥,提供充足营养');
+ if (ndviChange < -0.1) {
+ recommendations.push('NDVI下降明显,建议进行实地勘察');
+ }
+ } else {
+ recommendations.push('作物长势稳定,维持当前管理方案');
+ recommendations.push('定期监测植被指数变化趋势');
+ recommendations.push('根据生长阶段调整田间管理措施');
+ }
+
+ return recommendations;
+ }
+}
+
+// 导出数据源
+export const DATA_SOURCES: Record = {
+ 'Sentinel-2': {
+ name: 'Sentinel-2',
+ resolution: '10',
+ description: '欧洲航天局,高分辨率多光谱成像',
+ provider: 'ESA'
+ },
+ 'Landsat-8': {
+ name: 'Landsat-8',
+ resolution: '30',
+ description: '美国地质调查局,中分辨率多光谱成像',
+ provider: 'USGS'
+ },
+ 'MODIS': {
+ name: 'MODIS',
+ resolution: '250',
+ description: '中分辨率成像光谱仪,每日覆盖',
+ provider: 'NASA'
+ },
+ '高分一号': {
+ name: '高分一号',
+ resolution: '8',
+ description: '中国高分系列,高分辨率光学卫星',
+ provider: 'CNSA'
+ },
+ '天地图': {
+ name: '天地图',
+ resolution: '2',
+ description: '中国国产卫星影像服务',
+ provider: 'NASG'
+ }
+};
+
+// 工具函数
+export const formatImageDate = (dateString: string): string => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ });
+};
+
+export const getCloudCoverColorClass = (cloudCover: number): string => {
+ if (cloudCover <= 10) return 'bg-green-100 text-green-800 border-green-200';
+ if (cloudCover <= 30) return 'bg-yellow-100 text-yellow-800 border-yellow-200';
+ if (cloudCover <= 50) return 'bg-orange-100 text-orange-800 border-orange-200';
+ return 'bg-red-100 text-red-800 border-red-200';
+};
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/map/satellite/components/satelliteTypes.ts b/crop-x/src/app/(app)/land-information/map/satellite/components/satelliteTypes.ts
new file mode 100644
index 0000000..326732f
--- /dev/null
+++ b/crop-x/src/app/(app)/land-information/map/satellite/components/satelliteTypes.ts
@@ -0,0 +1,76 @@
+/**
+ * 卫星影像服务类型定义
+ */
+
+export interface SatelliteImage {
+ id: string;
+ date: string;
+ source: string;
+ resolution: number;
+ cloudCover: number;
+ ndvi: number;
+ evi: number;
+ lai: number;
+ season: string;
+ thumbnail: string;
+ url: string;
+}
+
+export interface ImageComparisonResult {
+ image1: SatelliteImage;
+ image2: SatelliteImage;
+ ndviChange: number;
+ eviChange: number;
+ changeType: 'improvement' | 'decline' | 'stable';
+ changeDescription: string;
+ recommendations: string[];
+}
+
+export interface TimeSeriesAnalysis {
+ dates: string[];
+ ndviValues: number[];
+ trend: 'increasing' | 'decreasing' | 'stable' | 'fluctuating';
+ growthStage: string;
+ healthScore: string;
+ alerts: string[];
+}
+
+export interface DataSource {
+ name: string;
+ resolution: string;
+ description: string;
+ provider: string;
+}
+
+export const DATA_SOURCES: Record = {
+ 'Sentinel-2': {
+ name: 'Sentinel-2',
+ resolution: '10',
+ description: '欧洲航天局,高分辨率多光谱成像',
+ provider: 'ESA'
+ },
+ 'Landsat-8': {
+ name: 'Landsat-8',
+ resolution: '30',
+ description: '美国地质调查局,中分辨率多光谱成像',
+ provider: 'USGS'
+ },
+ 'MODIS': {
+ name: 'MODIS',
+ resolution: '250',
+ description: '中分辨率成像光谱仪,每日覆盖',
+ provider: 'NASA'
+ },
+ '高分一号': {
+ name: '高分一号',
+ resolution: '8',
+ description: '中国高分系列,高分辨率光学卫星',
+ provider: 'CNSA'
+ },
+ '天地图': {
+ name: '天地图',
+ resolution: '2',
+ description: '中国国产卫星影像服务',
+ provider: 'NASG'
+ }
+};
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/land-information/map/satellite/page.tsx b/crop-x/src/app/(app)/land-information/map/satellite/page.tsx
index 0911a1f..4445fb3 100644
--- a/crop-x/src/app/(app)/land-information/map/satellite/page.tsx
+++ b/crop-x/src/app/(app)/land-information/map/satellite/page.tsx
@@ -1,18 +1,7 @@
'use client';
-import { Card } from '@/components/ui/card';
+import { FieldSatellite } from './components/FieldSatellite';
export default function SatellitePage() {
- return (
-
-
- 地块卫星影像
-
-
- 页面路径: /land-information/map/satellite
-
-
-
-
- );
+ return ;
}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/Navbar.tsx b/crop-x/src/components/layouts/Navbar.tsx
index 866d095..811c184 100644
--- a/crop-x/src/components/layouts/Navbar.tsx
+++ b/crop-x/src/components/layouts/Navbar.tsx
@@ -1,7 +1,7 @@
'use client';
import { Book, Menu, Sunset, Trees, Zap } from "lucide-react";
-import { Tractor, Map, Clipboard, Package, Brain, Droplets, Settings } from 'lucide-react';
+import { Sprout, Map, Clipboard, Package, Brain, Droplets, Settings } from 'lucide-react';
import { MessageBell } from './components/MessageBell';
import { UserProfile } from './components/UserProfile';
import { ThemeToggle } from './ThemeToggle';
@@ -117,11 +117,11 @@ const Navbar1 = ({ navbarData }: Navbar1Props) => {
-
-
+
+
-
智慧农业生产管理系统
+
智慧农业生产管理系统
Smart Agriculture Management System