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 index f578b22..98ce9e0 100644 --- 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 @@ -1,78 +1,74 @@ '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 { Button } from '@/components/ui/button'; 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'; +import { ReportGenerator } from './ReportGenerator'; interface FieldSelectorProps { - fields: FieldData[]; + availableFields: ReturnType['availableFields']; + comparisonFields: ReturnType['comparisonFields']; + onAddField: (fieldId: string) => void; + onRemoveField: (fieldId: string) => void; + onGenerateReport: () => void; + reportGenerating: boolean; + reportProgress: number; } -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个地块进行对比'); - } - }; - +export function FieldSelector({ + availableFields, + comparisonFields, + onAddField, + onRemoveField, + onGenerateReport, + reportGenerating, + reportProgress, +}: FieldSelectorProps) { return ( - -
-
- -
- {comparisonFields.map(field => ( - +
+ +
+ {comparisonFields.map(field => ( + + {field.name} + - - ))} - {availableFields.length > 0 && ( - - )} -
+ + + + ))} + {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 index 81488c6..dbf4135 100644 --- 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 @@ -2,14 +2,14 @@ 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'; +import { MapIcon, MapPin } from 'lucide-react'; +import { FieldData } from './chartComparisonReducer'; -export function MapComparison() { - const { state } = useChartAnalysis(); - - const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id)); +interface MapComparisonProps { + comparisonFields: FieldData[]; +} +export function MapComparison({ comparisonFields }: MapComparisonProps) { const getGradeColor = (grade: string) => { switch (grade) { case '高度适宜': return 'bg-green-500 text-white'; @@ -19,33 +19,14 @@ export function MapComparison() { } }; - // 如果没有选择地块,显示空状态 - if (comparisonFields.length === 0) { - return ( - -

- - 地块空间分布对比 -

-
-
- -

请先选择要对比的地块

-

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

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

地块空间分布对比

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

{field.name}

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 deleted file mode 100644 index 966002a..0000000 --- a/crop-x/src/app/(app)/land-information/comparison/chart/components/NutrientComparison.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'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 index 0bac777..7540eb1 100644 --- 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 @@ -1,8 +1,9 @@ 'use client'; import { Card } from '@/components/ui/card'; +import { Radar } from 'lucide-react'; import { - RadarChart, + RadarChart as RechartsRadarChart, Radar as RechartsRadar, PolarGrid, PolarAngleAxis, @@ -10,14 +11,13 @@ import { ResponsiveContainer, Legend, } from 'recharts'; -import { Radar } from 'lucide-react'; -import { FieldData, useChartAnalysis } from './chartAnalysisReducer'; +import { FieldData } from './chartComparisonReducer'; -export function ChartRadarAnalysis() { - const { state } = useChartAnalysis(); - - const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id)); +interface RadarChartProps { + comparisonFields: FieldData[]; +} +export function RadarChart({ comparisonFields }: RadarChartProps) { // 雷达图数据 const radarData = [ { @@ -64,32 +64,8 @@ export function ChartRadarAnalysis() { }, ]; - const colors = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444']; - - // 如果没有选择地块,显示空状态 - if (comparisonFields.length === 0) { - return ( - -

- - 多维度雷达图对比 -

-

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

-
-
- -

请先选择要对比的地块

-

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

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

多维度雷达图对比 @@ -99,22 +75,25 @@ export function ChartRadarAnalysis() {

- + - {comparisonFields.map((field, index) => ( - - ))} + {comparisonFields.map((field, index) => { + const colors = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444']; + return ( + + ); + })} - +
diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/ReportGenerator.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/ReportGenerator.tsx new file mode 100644 index 0000000..5356e2a --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/ReportGenerator.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { FileText } from 'lucide-react'; +import { useChartComparison } from './chartComparisonReducer'; + +interface ReportGeneratorProps { + onGenerateReport: () => void; + reportGenerating: boolean; + reportProgress: number; +} + +export function ReportGenerator({ onGenerateReport, reportGenerating, reportProgress }: ReportGeneratorProps) { + return ( +
+ + + + {reportGenerating && ( +
+
+ 生成进度 + {reportProgress}% +
+ +

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

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/comparison/chart/components/ReportList.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/ReportList.tsx new file mode 100644 index 0000000..89597fe --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/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, X, Download } from 'lucide-react'; +import { ComparisonReport } from './chartComparisonReducer'; + +interface ReportListProps { + savedReports: ComparisonReport[]; + onDeleteReport: (reportId: string) => void; + onDownloadPDF: (report: ComparisonReport) => void; + onDownloadWord: (report: ComparisonReport) => void; +} + +export function ReportList({ + savedReports, + onDeleteReport, + onDownloadPDF, + onDownloadWord, +}: 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/chart/components/YieldComparison.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/YieldComparison.tsx deleted file mode 100644 index de9e7f8..0000000 --- a/crop-x/src/app/(app)/land-information/comparison/chart/components/YieldComparison.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'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/YieldNutrientCharts.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/YieldNutrientCharts.tsx new file mode 100644 index 0000000..08e2b79 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/YieldNutrientCharts.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { Card } from '@/components/ui/card'; +import { BarChart3, TrendingUp, Scale } from 'lucide-react'; +import { + ResponsiveContainer, + BarChart as RechartsBarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + LineChart as RechartsLineChart, + Line, +} from 'recharts'; +import { FieldData } from './chartComparisonReducer'; + +interface YieldNutrientChartsProps { + comparisonFields: FieldData[]; +} + +export function YieldNutrientCharts({ comparisonFields }: YieldNutrientChartsProps) { + // 产量对比数据 + const yieldData = comparisonFields.map(field => ({ + name: field.name, + 产量: field.yield, + 有机质: field.organicMatter, + })); + + // 养分对比数据 + const nutrientData = comparisonFields.map(field => ({ + name: field.name, + 全氮: field.nitrogen, + 全磷: field.phosphorus, + 全钾: field.potassium, + })); + + return ( +
+ {/* 产量与有机质对比 */} +
+ +

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

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

+ + 有机质含量对比 (g/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 deleted file mode 100644 index 966c3ba..0000000 --- a/crop-x/src/app/(app)/land-information/comparison/chart/components/chartAnalysisReducer.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'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/components/chartComparisonReducer.tsx b/crop-x/src/app/(app)/land-information/comparison/chart/components/chartComparisonReducer.tsx new file mode 100644 index 0000000..3d98405 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/comparison/chart/components/chartComparisonReducer.tsx @@ -0,0 +1,350 @@ +'use client'; + +import { useReducer, useEffect } 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 ChartComparisonState { + selectedFields: string[]; + fieldsData: FieldData[]; + isInitializing: boolean; + reportGenerating: boolean; + reportProgress: number; + savedReports: ComparisonReport[]; +} + +// Action类型 +export type ChartComparisonAction = + | { type: 'SET_INITIALIZING'; payload: boolean } + | { type: 'SET_SELECTED_FIELDS'; payload: string[] } + | { type: 'ADD_FIELD'; payload: string } + | { type: 'REMOVE_FIELD'; payload: string } + | { type: 'SET_FIELDS_DATA'; payload: FieldData[] } + | { type: 'SET_REPORT_GENERATING'; payload: boolean } + | { type: 'SET_REPORT_PROGRESS'; payload: number } + | { type: 'ADD_SAVED_REPORT'; payload: ComparisonReport } + | { type: 'DELETE_REPORT'; payload: string }; + +// 初始状态 +const initialState: ChartComparisonState = { + selectedFields: ['field-1', 'field-2'], + fieldsData: [], + isInitializing: true, + 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: [], + }, + ], +}; + +// Reducer函数 +export function chartComparisonReducer( + state: ChartComparisonState, + action: ChartComparisonAction +): ChartComparisonState { + switch (action.type) { + case 'SET_INITIALIZING': + return { + ...state, + isInitializing: action.payload, + }; + 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_FIELDS_DATA': + return { + ...state, + fieldsData: 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(report => report.id !== action.payload), + }; + default: + return state; + } +} + +// 模拟地块数据 +export 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: '良好', + }, +]; + +// 自定义Hook +export function useChartComparison() { + const [state, dispatch] = useReducer(chartComparisonReducer, initialState); + + // 初始化数据 + useEffect(() => { + const initializeData = () => { + dispatch({ type: 'SET_INITIALIZING', payload: true }); + dispatch({ type: 'SET_FIELDS_DATA', payload: mockFieldsData }); + dispatch({ type: 'SET_INITIALIZING', payload: false }); + }; + + initializeData(); + }, []); + + // 计算可用地块和已选地块 + const availableFields = state.fieldsData.filter(f => !state.selectedFields.includes(f.id)); + const comparisonFields = state.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 currentProgress = 0; + const interval = setInterval(() => { + currentProgress += 10; + dispatch({ type: 'SET_REPORT_PROGRESS', payload: currentProgress }); + + if (currentProgress >= 100) { + clearInterval(interval); + + // 保存新报告 + 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, + }; + + // 延迟一下再设置生成完成,让用户看到100% + setTimeout(() => { + dispatch({ type: 'SET_REPORT_GENERATING', payload: false }); + dispatch({ type: 'ADD_SAVED_REPORT', payload: newReport }); + toast.success('对比分析报告已生成!'); + }, 300); + } + }, 200); + }; + + // 删除报告 + const handleDeleteReport = (reportId: string) => { + dispatch({ type: 'DELETE_REPORT', payload: reportId }); + toast.success('报告已删除'); + }; + + // 下载PDF报告 + 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.success(`PDF报告"${reportData.name}"已生成完成!`, { + description: '报告包含执行摘要、详细数据对比和智能分析建议', + duration: 4000, + }); + }; + + // 下载Word报告 + 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.success(`Word报告"${reportData.name}"已生成完成!`, { + description: '报告包含执行摘要、详细数据对比和智能分析建议', + duration: 4000, + }); + }; + + return { + state, + dispatch, + availableFields, + comparisonFields, + handleAddField, + handleRemoveField, + handleGenerateReport, + handleDeleteReport, + handleDownloadPDF, + handleDownloadWord, + }; +} \ 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 0d81786..028b30a 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,126 +1,43 @@ 'use client'; -import { useEffect } from 'react'; import { Card } from '@/components/ui/card'; -import { toast } from 'sonner'; +import { useChartComparison } from './components/chartComparisonReducer'; import { FieldSelector } from './components/FieldSelector'; -import { ChartRadarAnalysis } from './components/RadarChart'; -import { YieldComparison } from './components/YieldComparison'; -import { NutrientComparison } from './components/NutrientComparison'; +import { RadarChart } from './components/RadarChart'; +import { YieldNutrientCharts } from './components/YieldNutrientCharts'; 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: '良好', - }, -]; +import { ReportList } from './components/ReportList'; export default function ChartPage() { - const { setFields, state } = useChartAnalysis(); + const { + state, + availableFields, + comparisonFields, + handleAddField, + handleRemoveField, + handleGenerateReport, + handleDeleteReport, + handleDownloadPDF, + handleDownloadWord, + } = useChartComparison(); - // 初始化数据 - useEffect(() => { - // 直接使用模拟数据,确保数据可用 - setFields(mockFieldsData); - localStorage.setItem('chart-analysis-fields', JSON.stringify(mockFieldsData)); - }, [setFields]); - - // 如果数据还没有加载,显示loading状态 - if (state.fields.length === 0) { + if (state.isInitializing) { return (
-

可视化图表分析

+

可视化图表分析

- 多维度指标可视化展示与对比分析 + 多维度指标看板、可视化图表分析与智能对比报告

- -
-

正在加载数据...

+
+
+
+

正在初始化数据...

- +
); } @@ -129,30 +46,74 @@ export default function ChartPage() {
-

可视化图表分析

+

可视化图表分析

- 多维度指标可视化展示与对比分析 + 多维度指标看板、可视化图表分析与智能对比报告

{/* 地块选择器 */} - + + + - {/* 图表分析区域 */} + {/* 可视化图表分析 */}
{/* 雷达图 */} - + - {/* 产量与有机质对比 */} - - - {/* 养分对比 */} - + {/* 产量与养分对比 */} + {/* 地图对比 */} - +
+ + {/* 历史报告列表 */} + + + {/* 使用说明 */} + +

💡 报告功能说明

+
+
+

📊 报告内容

+
    +
  • • 执行摘要:最优地块、最高产量、最佳有机质分析
  • +
  • • 详细数据对比:各地块完整数据表格
  • +
  • • 智能分析建议:基于数据的对比分析和改进建议
  • +
+
+
+

🎯 使用方式

+
    +
  • • 选择2-4个地块进行对比分析
  • +
  • • 点击"生成报告"创建分析报告
  • +
  • • 支持PDF和Word格式导出
  • +
  • • 历史报告可在下方查看和管理
  • +
+
+
+
+

+ 提示:点击下载按钮后,报告会立即生成并在右下角显示成功提示。报告包含完整的地块对比分析数据和专业建议。 +

+
+
); } \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/risk/disposal/components/FieldFilter.tsx b/crop-x/src/app/(app)/land-information/risk/disposal/components/FieldFilter.tsx new file mode 100644 index 0000000..2fdebbc --- /dev/null +++ b/crop-x/src/app/(app)/land-information/risk/disposal/components/FieldFilter.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Card } from '@/components/ui/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +interface FieldFilterProps { + selectedField: string; + onFieldChange: (value: string) => void; +} + +export function FieldFilter({ selectedField, onFieldChange }: FieldFilterProps) { + return ( + +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/risk/disposal/components/StatusStats.tsx b/crop-x/src/app/(app)/land-information/risk/disposal/components/StatusStats.tsx new file mode 100644 index 0000000..dfe56cd --- /dev/null +++ b/crop-x/src/app/(app)/land-information/risk/disposal/components/StatusStats.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { AlertCircle, Eye, Activity, CheckCircle2 } from 'lucide-react'; + +interface StatusStatsProps { + filteredWarnings: Array<{ + status: '未查看' | '已查看' | '处理中' | '已完成'; + }>; +} + +export function StatusStats({ filteredWarnings }: StatusStatsProps) { + const stats = { + 未查看: filteredWarnings.filter(w => w.status === '未查看').length, + 已查看: filteredWarnings.filter(w => w.status === '已查看').length, + 处理中: filteredWarnings.filter(w => w.status === '处理中').length, + 已完成: filteredWarnings.filter(w => w.status === '已完成').length, + }; + + return ( +
+ +
+
+

未查看

+

{stats.未查看}

+
+ +
+
+ + +
+
+

已查看

+

{stats.已查看}

+
+ +
+
+ + +
+
+

处理中

+

{stats.处理中}

+
+ +
+
+ + +
+
+

已完成

+

{stats.已完成}

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/risk/disposal/components/WarningList.tsx b/crop-x/src/app/(app)/land-information/risk/disposal/components/WarningList.tsx new file mode 100644 index 0000000..67e9789 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/risk/disposal/components/WarningList.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { Eye, Clock, FileText, CheckCircle2, User, Activity } from 'lucide-react'; +import { WarningRecord } from './disposalReducer'; + +interface WarningListProps { + filteredWarnings: WarningRecord[]; + getRiskTypeIcon: (type: string) => string; + getRiskLevelColor: (level: string) => string; + getStatusIcon: (status: string) => React.ReactNode; + getStatusColor: (status: string) => string; + onViewWarning: (warning: WarningRecord) => void; +} + +export function WarningList({ + filteredWarnings, + getRiskTypeIcon, + getRiskLevelColor, + getStatusIcon, + getStatusColor, + onViewWarning, +}: WarningListProps) { + const getProgressPercentage = (status: string) => { + switch (status) { + case '未查看': return 25; + case '已查看': return 50; + case '处理中': return 75; + case '已完成': return 100; + default: return 0; + } + }; + + const getProgressColor = (status: string) => { + switch (status) { + case '未查看': return 'bg-red-500'; + case '已查看': return 'bg-blue-500'; + case '处理中': return 'bg-orange-500'; + case '已完成': return 'bg-green-500'; + default: return 'bg-gray-500'; + } + }; + + const getProgressBgColor = (status: string) => { + switch (status) { + case '未查看': return 'bg-red-200'; + case '已查看': return 'bg-blue-200'; + case '处理中': return 'bg-orange-200'; + case '已完成': return 'bg-green-200'; + default: return 'bg-gray-200'; + } + }; + + return ( + +

+ + 预警处置跟踪 +

+
+ {filteredWarnings.map(warning => ( + +
+
+
{getRiskTypeIcon(warning.riskType)}
+
+
+

{warning.fieldName}

+ + {warning.riskLevel} + + + {warning.riskType} + +
+

+ 预警时间: {warning.warningTime} +

+
+
+
+ {getStatusIcon(warning.status)} + {warning.status} +
+
+ +
+
+

责任人

+

+ + {warning.responsible} +

+
+
+

接收人

+

{warning.recipients.join(', ')}

+
+
+ + {/* 处置流程进度 */} +
+
+ 处置进度 + + {warning.timeline?.length || 0} 个节点 + +
+
+
+
+
+ {getProgressPercentage(warning.status)}% +
+
+ + {warning.viewTime && ( +
+

+ + 查看时间 +

+

{warning.viewTime}

+
+ )} + + {warning.disposalPlan && ( +
+

+ + 处置方案 +

+

{warning.disposalPlan}

+ {warning.disposalPlanSubmitTime && ( +

+ 提交于: {warning.disposalPlanSubmitTime} +

+ )} +
+ )} + + {warning.disposalResult && ( +
+

+ + 处置结果 +

+

{warning.disposalResult}

+

+ 完成时间: {warning.completedTime} +

+ {warning.effectEvaluation && ( +
+

效果评估

+

{warning.effectEvaluation}

+
+ )} +
+ )} + +
+ + {warning.timeline && warning.timeline.length > 0 && ( + + + {warning.timeline.length} 条记录 + + )} +
+ + ))} +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/land-information/risk/disposal/components/WorkflowDialog.tsx b/crop-x/src/app/(app)/land-information/risk/disposal/components/WorkflowDialog.tsx new file mode 100644 index 0000000..7462236 --- /dev/null +++ b/crop-x/src/app/(app)/land-information/risk/disposal/components/WorkflowDialog.tsx @@ -0,0 +1,394 @@ +'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 { Textarea } from '@/components/ui/textarea'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Eye, Activity, CheckCircle2, Clock, User, FileText } from 'lucide-react'; +import { WarningRecord } from './disposalReducer'; + +interface WorkflowDialogProps { + isOpen: boolean; + onClose: () => void; + selectedWarning: WarningRecord | null; + disposalPlan: string; + disposalResult: string; + processingNotes: string; + resources: string; + actualCost: string; + effectEvaluation: string; + onDisposalPlanChange: (value: string) => void; + onDisposalResultChange: (value: string) => void; + onProcessingNotesChange: (value: string) => void; + onResourcesChange: (value: string) => void; + onActualCostChange: (value: string) => void; + onEffectEvaluationChange: (value: string) => void; + onUpdateStatus: (status: '未查看' | '已查看' | '处理中' | '已完成') => void; + getRiskTypeIcon: (type: string) => string; + getRiskLevelColor: (level: string) => string; + getStatusIcon: (status: string) => React.ReactNode; + getStatusColor: (status: string) => string; +} + +export function WorkflowDialog({ + isOpen, + onClose, + selectedWarning, + disposalPlan, + disposalResult, + processingNotes, + resources, + actualCost, + effectEvaluation, + onDisposalPlanChange, + onDisposalResultChange, + onProcessingNotesChange, + onResourcesChange, + onActualCostChange, + onEffectEvaluationChange, + onUpdateStatus, + getRiskTypeIcon, + getRiskLevelColor, + getStatusIcon, + getStatusColor, +}: WorkflowDialogProps) { + if (!selectedWarning) return null; + + return ( + + + + 预警处置工作流 + +
+ {/* 基本信息 */} +
+
+
+

地块名称

+

{selectedWarning.fieldName}

+
+
+

风险类型

+
+ {getRiskTypeIcon(selectedWarning.riskType)} + {selectedWarning.riskType} +
+
+
+

风险等级

+ + {selectedWarning.riskLevel} + +
+
+

预警时间

+

{selectedWarning.warningTime}

+
+
+

责任人

+

+ + {selectedWarning.responsible} +

+
+
+

当前状态

+
+ {getStatusIcon(selectedWarning.status)} + {selectedWarning.status} +
+
+
+
+ + + + 处置流程 + 时间线 + 详细信息 + + + + {/* 1. 查看确认 */} + +
+
+ 1 +
+
+

接收查看

+

责任人确认接收预警信息

+
+
+ {selectedWarning.viewTime && ( +
+ ✓ 已查看于 {selectedWarning.viewTime} +
+ )} + {selectedWarning.status === '未查看' && ( +
+ +
+ )} +
+ + {/* 2. 制定方案 */} + +
+
+ 2 +
+
+

制定方案

+

提交详细的处置方案

+
+
+ {selectedWarning.disposalPlan ? ( +
+
+

处置方案

+

{selectedWarning.disposalPlan}

+
+ {selectedWarning.disposalPlanSubmitTime && ( +

+ ✓ 提交于 {selectedWarning.disposalPlanSubmitTime} +

+ )} +
+ ) : selectedWarning.status === '已查看' ? ( +
+
+ +