生产管理系统前端 - 地块风险预警开发

This commit is contained in:
2025-10-30 16:15:15 +08:00
parent 77bf48f88a
commit 5d5a24ac89
22 changed files with 3658 additions and 563 deletions

View File

@@ -1,40 +1,31 @@
'use client'; '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 { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Plus, X } from 'lucide-react'; import { Plus, X } from 'lucide-react';
import { toast } from 'sonner'; import { ReportGenerator } from './ReportGenerator';
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
interface FieldSelectorProps { interface FieldSelectorProps {
fields: FieldData[]; availableFields: ReturnType<typeof useChartComparison>['availableFields'];
comparisonFields: ReturnType<typeof useChartComparison>['comparisonFields'];
onAddField: (fieldId: string) => void;
onRemoveField: (fieldId: string) => void;
onGenerateReport: () => void;
reportGenerating: boolean;
reportProgress: number;
} }
export function FieldSelector({ fields }: FieldSelectorProps) { export function FieldSelector({
const { state, addField, removeField } = useChartAnalysis(); availableFields,
comparisonFields,
const availableFields = fields.filter(f => !state.selectedFields.includes(f.id)); onAddField,
const comparisonFields = fields.filter(f => state.selectedFields.includes(f.id)); onRemoveField,
onGenerateReport,
const handleAddField = (fieldId: string) => { reportGenerating,
const success = addField(fieldId); reportProgress,
if (!success) { }: FieldSelectorProps) {
toast.error('最多只能同时对比4个地块');
}
};
const handleRemoveField = (fieldId: string) => {
const success = removeField(fieldId);
if (!success) {
toast.error('至少需要选择2个地块进行对比');
}
};
return ( return (
<Card className="p-4 bg-card">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="flex-1"> <div className="flex-1">
<label className="text-xs text-muted-foreground mb-2 block"> (2-4)</label> <label className="text-xs text-muted-foreground mb-2 block"> (2-4)</label>
@@ -42,19 +33,19 @@ export function FieldSelector({ fields }: FieldSelectorProps) {
{comparisonFields.map(field => ( {comparisonFields.map(field => (
<Badge <Badge
key={field.id} key={field.id}
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-3 py-1.5 flex items-center gap-2 font-light" className="bg-green-100 text-green-800 px-3 py-1.5 flex items-center gap-2 font-light"
> >
{field.name} {field.name}
<button <button
onClick={() => handleRemoveField(field.id)} onClick={() => onRemoveField(field.id)}
className="hover:bg-green-200 dark:hover:bg-green-800 rounded-full p-0.5" className="hover:bg-green-200 rounded-full p-0.5"
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</button> </button>
</Badge> </Badge>
))} ))}
{availableFields.length > 0 && ( {availableFields.length > 0 && (
<Select onValueChange={handleAddField}> <Select onValueChange={onAddField}>
<SelectTrigger className="w-40"> <SelectTrigger className="w-40">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@@ -72,7 +63,12 @@ export function FieldSelector({ fields }: FieldSelectorProps) {
)} )}
</div> </div>
</div> </div>
<ReportGenerator
onGenerateReport={onGenerateReport}
reportGenerating={reportGenerating}
reportProgress={reportProgress}
/>
</div> </div>
</Card>
); );
} }

View File

@@ -2,14 +2,14 @@
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Map as MapIcon, MapPin } from 'lucide-react'; import { MapIcon, MapPin } from 'lucide-react';
import { FieldData, useChartAnalysis } from './chartAnalysisReducer'; import { FieldData } from './chartComparisonReducer';
export function MapComparison() { interface MapComparisonProps {
const { state } = useChartAnalysis(); comparisonFields: FieldData[];
}
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
export function MapComparison({ comparisonFields }: MapComparisonProps) {
const getGradeColor = (grade: string) => { const getGradeColor = (grade: string) => {
switch (grade) { switch (grade) {
case '高度适宜': return 'bg-green-500 text-white'; case '高度适宜': return 'bg-green-500 text-white';
@@ -19,33 +19,14 @@ export function MapComparison() {
} }
}; };
// 如果没有选择地块,显示空状态
if (comparisonFields.length === 0) {
return ( return (
<Card className="p-6 bg-card"> <Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<MapIcon className="w-5 h-5 text-blue-600" />
</h3>
<div className="h-64 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<MapIcon className="w-16 h-16 mx-auto mb-4 opacity-50" />
<p></p>
<p className="text-sm mt-2"></p>
</div>
</div>
</Card>
);
}
return (
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2"> <h3 className="mb-4 flex items-center gap-2">
<MapIcon className="w-5 h-5 text-blue-600" /> <MapIcon className="w-5 h-5 text-blue-600" />
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{comparisonFields.map((field, index) => ( {comparisonFields.map((field) => (
<div key={field.id} className="space-y-2"> <div key={field.id} className="space-y-2">
<h4 className="text-sm font-medium">{field.name}</h4> <h4 className="text-sm font-medium">{field.name}</h4>
<div className="h-48 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900 dark:to-green-800 rounded-lg flex items-center justify-center relative overflow-hidden"> <div className="h-48 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900 dark:to-green-800 rounded-lg flex items-center justify-center relative overflow-hidden">

View File

@@ -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 (
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2">
<Scale className="w-5 h-5 text-orange-600" />
(g/kg)
</h3>
<div className="h-80 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<Scale className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm"></p>
</div>
</div>
</Card>
);
}
return (
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2">
<Scale className="w-5 h-5 text-orange-600" />
(g/kg)
</h3>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={nutrientData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="全氮" stroke="#10b981" strokeWidth={2} />
<Line type="monotone" dataKey="全磷" stroke="#3b82f6" strokeWidth={2} />
<Line type="monotone" dataKey="全钾" stroke="#f59e0b" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
);
}

View File

@@ -1,8 +1,9 @@
'use client'; 'use client';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Radar } from 'lucide-react';
import { import {
RadarChart, RadarChart as RechartsRadarChart,
Radar as RechartsRadar, Radar as RechartsRadar,
PolarGrid, PolarGrid,
PolarAngleAxis, PolarAngleAxis,
@@ -10,14 +11,13 @@ import {
ResponsiveContainer, ResponsiveContainer,
Legend, Legend,
} from 'recharts'; } from 'recharts';
import { Radar } from 'lucide-react'; import { FieldData } from './chartComparisonReducer';
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
export function ChartRadarAnalysis() { interface RadarChartProps {
const { state } = useChartAnalysis(); comparisonFields: FieldData[];
}
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
export function RadarChart({ comparisonFields }: RadarChartProps) {
// 雷达图数据 // 雷达图数据
const radarData = [ const radarData = [
{ {
@@ -64,32 +64,8 @@ export function ChartRadarAnalysis() {
}, },
]; ];
const colors = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'];
// 如果没有选择地块,显示空状态
if (comparisonFields.length === 0) {
return ( return (
<Card className="p-6 bg-card"> <Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<Radar className="w-5 h-5 text-blue-600" />
</h3>
<p className="text-sm text-muted-foreground mb-4">
</p>
<div className="h-96 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<Radar className="w-16 h-16 mx-auto mb-4 opacity-50" />
<p></p>
<p className="text-sm mt-2"></p>
</div>
</div>
</Card>
);
}
return (
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2"> <h3 className="mb-4 flex items-center gap-2">
<Radar className="w-5 h-5 text-blue-600" /> <Radar className="w-5 h-5 text-blue-600" />
@@ -99,11 +75,13 @@ export function ChartRadarAnalysis() {
</p> </p>
<div className="h-96"> <div className="h-96">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<RadarChart data={radarData}> <RechartsRadarChart data={radarData}>
<PolarGrid /> <PolarGrid />
<PolarAngleAxis dataKey="indicator" /> <PolarAngleAxis dataKey="indicator" />
<PolarRadiusAxis angle={90} domain={[0, 100]} /> <PolarRadiusAxis angle={90} domain={[0, 100]} />
{comparisonFields.map((field, index) => ( {comparisonFields.map((field, index) => {
const colors = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'];
return (
<RechartsRadar <RechartsRadar
key={field.id} key={field.id}
name={field.name} name={field.name}
@@ -112,9 +90,10 @@ export function ChartRadarAnalysis() {
fill={colors[index]} fill={colors[index]}
fillOpacity={0.3} fillOpacity={0.3}
/> />
))} );
})}
<Legend /> <Legend />
</RadarChart> </RechartsRadarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</Card> </Card>

View File

@@ -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 (
<div className="flex-shrink-0">
<label className="text-xs text-muted-foreground mb-2 block opacity-0"></label>
<Button
className="bg-green-600 hover:bg-green-700"
onClick={onGenerateReport}
disabled={reportGenerating}
>
<FileText className="w-4 h-4 mr-2" />
</Button>
{reportGenerating && (
<div className="mt-4 pt-4 border-t">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-muted-foreground"></span>
<span className="font-medium">{reportProgress}%</span>
</div>
<Progress value={reportProgress} className="h-2" />
<p className="text-xs text-muted-foreground text-center mt-2">
...
</p>
</div>
)}
</div>
);
}

View File

@@ -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 (
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2">
<FileText className="w-5 h-5 text-green-600" />
</h3>
{savedReports.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<FileText className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p></p>
<p className="text-sm mt-1">"生成报告"</p>
</div>
) : (
<div className="space-y-3">
{savedReports.map((report) => (
<div
key={report.id}
className="p-4 border rounded-lg hover:bg-muted transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="mb-2 flex items-center gap-2">
<FileText className="w-4 h-4 text-green-600" />
{report.name}
</h4>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
: {report.generatedTime}
</span>
<span className="flex items-center gap-1">
<MapPin className="w-3 h-3" />
: {report.comparedFields.join('、')}
</span>
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<Button
size="sm"
variant="outline"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => onDownloadPDF(report)}
>
<Download className="w-4 h-4 mr-1" />
PDF
</Button>
<Button
size="sm"
variant="outline"
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
onClick={() => onDownloadWord(report)}
>
<Download className="w-4 h-4 mr-1" />
Word
</Button>
<Button
size="sm"
variant="outline"
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
onClick={() => onDeleteReport(report.id)}
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</Card>
);
}

View File

@@ -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 (
<div className="grid grid-cols-2 gap-4">
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-green-600" />
(kg/)
</h3>
<div className="h-72 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<BarChart3 className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm"></p>
</div>
</div>
</Card>
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-purple-600" />
(g/kg)
</h3>
<div className="h-72 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<TrendingUp className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm"></p>
</div>
</div>
</Card>
</div>
);
}
return (
<div className="grid grid-cols-2 gap-4">
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-green-600" />
(kg/)
</h3>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={yieldData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="产量" fill="#10b981" />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
<Card className="p-6 bg-card">
<h3 className="mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-purple-600" />
(g/kg)
</h3>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={yieldData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="有机质" fill="#8b5cf6" />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
</div>
);
}

View File

@@ -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 (
<div className="space-y-4">
{/* 产量与有机质对比 */}
<div className="grid grid-cols-2 gap-4">
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-green-600" />
(kg/)
</h3>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart data={yieldData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="产量" fill="#10b981" />
</RechartsBarChart>
</ResponsiveContainer>
</div>
</Card>
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-purple-600" />
(g/kg)
</h3>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart data={yieldData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="有机质" fill="#8b5cf6" />
</RechartsBarChart>
</ResponsiveContainer>
</div>
</Card>
</div>
{/* 养分对比 */}
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<Scale className="w-5 h-5 text-orange-600" />
(g/kg)
</h3>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<RechartsLineChart data={nutrientData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="全氮" stroke="#10b981" strokeWidth={2} />
<Line type="monotone" dataKey="全磷" stroke="#3b82f6" strokeWidth={2} />
<Line type="monotone" dataKey="全钾" stroke="#f59e0b" strokeWidth={2} />
</RechartsLineChart>
</ResponsiveContainer>
</div>
</Card>
</div>
);
}

View File

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

View File

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

View File

@@ -1,126 +1,43 @@
'use client'; 'use client';
import { useEffect } from 'react';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { toast } from 'sonner'; import { useChartComparison } from './components/chartComparisonReducer';
import { FieldSelector } from './components/FieldSelector'; import { FieldSelector } from './components/FieldSelector';
import { ChartRadarAnalysis } from './components/RadarChart'; import { RadarChart } from './components/RadarChart';
import { YieldComparison } from './components/YieldComparison'; import { YieldNutrientCharts } from './components/YieldNutrientCharts';
import { NutrientComparison } from './components/NutrientComparison';
import { MapComparison } from './components/MapComparison'; import { MapComparison } from './components/MapComparison';
import { useChartAnalysis, FieldData } from './components/chartAnalysisReducer'; import { ReportList } from './components/ReportList';
// 模拟地块数据
const mockFieldsData: FieldData[] = [
{
id: 'field-1',
name: '东区1号地',
area: 50.5,
location: '东经120.15°, 北纬30.25°',
soilType: '壤土',
ph: 6.5,
organicMatter: 32,
nitrogen: 1.8,
phosphorus: 1.2,
potassium: 18,
soilDepth: 85,
slope: 3,
currentCrop: '水稻',
yield: 750,
suitabilityScore: 87,
suitabilityGrade: '高度适宜',
irrigation: '喷灌',
drainage: '良好',
},
{
id: 'field-2',
name: '西区2号地',
area: 45.2,
location: '东经120.12°, 北纬30.28°',
soilType: '粘土',
ph: 7.8,
organicMatter: 22,
nitrogen: 1.3,
phosphorus: 0.9,
potassium: 14,
soilDepth: 55,
slope: 5,
currentCrop: '玉米',
yield: 650,
suitabilityScore: 72,
suitabilityGrade: '一般适宜',
irrigation: '滴灌',
drainage: '中等',
},
{
id: 'field-3',
name: '南区3号地',
area: 38.8,
location: '东经120.18°, 北纬30.22°',
soilType: '砂土',
ph: 8.5,
organicMatter: 15,
nitrogen: 0.8,
phosphorus: 0.6,
potassium: 10,
soilDepth: 42,
slope: 8,
currentCrop: '小麦',
yield: 480,
suitabilityScore: 58,
suitabilityGrade: '不适宜',
irrigation: '漫灌',
drainage: '较差',
},
{
id: 'field-4',
name: '北区4号地',
area: 55.0,
location: '东经120.20°, 北纬30.30°',
soilType: '壤土',
ph: 6.8,
organicMatter: 28,
nitrogen: 1.6,
phosphorus: 1.0,
potassium: 16,
soilDepth: 75,
slope: 2,
currentCrop: '大豆',
yield: 380,
suitabilityScore: 82,
suitabilityGrade: '高度适宜',
irrigation: '喷灌',
drainage: '良好',
},
];
export default function ChartPage() { export default function ChartPage() {
const { setFields, state } = useChartAnalysis(); const {
state,
availableFields,
comparisonFields,
handleAddField,
handleRemoveField,
handleGenerateReport,
handleDeleteReport,
handleDownloadPDF,
handleDownloadWord,
} = useChartComparison();
// 初始化数据 if (state.isInitializing) {
useEffect(() => {
// 直接使用模拟数据,确保数据可用
setFields(mockFieldsData);
localStorage.setItem('chart-analysis-fields', JSON.stringify(mockFieldsData));
}, [setFields]);
// 如果数据还没有加载显示loading状态
if (state.fields.length === 0) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-green-800 dark:text-green-200"></h2> <h2 className="text-green-800"></h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
</p> </p>
</div> </div>
</div> </div>
<Card className="p-6 bg-card"> <div className="flex items-center justify-center py-12">
<div className="text-center py-8"> <div className="text-center">
<p>...</p> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600 mx-auto mb-4"></div>
<p className="text-muted-foreground">...</p>
</div>
</div> </div>
</Card>
</div> </div>
); );
} }
@@ -129,30 +46,74 @@ export default function ChartPage() {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-green-800 dark:text-green-200"></h2> <h2 className="text-green-800"></h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
</p> </p>
</div> </div>
</div> </div>
{/* 地块选择器 */} {/* 地块选择器 */}
<FieldSelector fields={state.fields} /> <Card className="p-4 bg-card">
<FieldSelector
availableFields={availableFields}
comparisonFields={comparisonFields}
onAddField={handleAddField}
onRemoveField={handleRemoveField}
onGenerateReport={handleGenerateReport}
reportGenerating={state.reportGenerating}
reportProgress={state.reportProgress}
/>
</Card>
{/* 图表分析区域 */} {/* 可视化图表分析 */}
<div className="space-y-4"> <div className="space-y-4">
{/* 雷达图 */} {/* 雷达图 */}
<ChartRadarAnalysis /> <RadarChart comparisonFields={comparisonFields} />
{/* 产量与有机质对比 */} {/* 产量与养分对比 */}
<YieldComparison /> <YieldNutrientCharts comparisonFields={comparisonFields} />
{/* 养分对比 */}
<NutrientComparison />
{/* 地图对比 */} {/* 地图对比 */}
<MapComparison /> <MapComparison comparisonFields={comparisonFields} />
</div> </div>
{/* 历史报告列表 */}
<ReportList
savedReports={state.savedReports}
onDeleteReport={handleDeleteReport}
onDownloadPDF={handleDownloadPDF}
onDownloadWord={handleDownloadWord}
/>
{/* 使用说明 */}
<Card className="p-6 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<h3 className="mb-3 text-blue-800 dark:text-blue-200">💡 </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-700 dark:text-blue-300">
<div>
<h4 className="font-medium mb-2">📊 </h4>
<ul className="space-y-1 text-xs">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">🎯 使</h4>
<ul className="space-y-1 text-xs">
<li> 2-4</li>
<li> "生成报告"</li>
<li> PDF和Word格式导出</li>
<li> </li>
</ul>
</div>
</div>
<div className="mt-4 p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
<p className="text-xs text-blue-600 dark:text-blue-400">
<strong></strong>
</p>
</div>
</Card>
</div> </div>
); );
} }

View File

@@ -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 (
<Card className="p-4 bg-card">
<div className="flex items-center gap-4">
<label className="text-sm font-medium">:</label>
<Select value={selectedField} onValueChange={onFieldChange}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="field-1">1</SelectItem>
<SelectItem value="field-2">西2</SelectItem>
<SelectItem value="field-3">3</SelectItem>
<SelectItem value="field-4">4</SelectItem>
</SelectContent>
</Select>
</div>
</Card>
);
}

View File

@@ -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 (
<div className="grid grid-cols-4 gap-4">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-red-600">{stats.}</p>
</div>
<AlertCircle className="w-12 h-12 text-red-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-blue-600">{stats.}</p>
</div>
<Eye className="w-12 h-12 text-blue-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-orange-600">{stats.}</p>
</div>
<Activity className="w-12 h-12 text-orange-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-green-600">{stats.}</p>
</div>
<CheckCircle2 className="w-12 h-12 text-green-600 opacity-50" />
</div>
</Card>
</div>
);
}

View File

@@ -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 (
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<Activity className="w-5 h-5 text-purple-600" />
</h3>
<div className="space-y-4">
{filteredWarnings.map(warning => (
<Card key={warning.id} className="p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3">
<div className="flex items-start gap-3">
<div className="text-2xl">{getRiskTypeIcon(warning.riskType)}</div>
<div>
<div className="flex items-center gap-2 mb-1">
<h4>{warning.fieldName}</h4>
<Badge className={getRiskLevelColor(warning.riskLevel)}>
{warning.riskLevel}
</Badge>
<Badge variant="outline" className="text-xs">
{warning.riskType}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
: {warning.warningTime}
</p>
</div>
</div>
<div className={`px-3 py-1.5 rounded-full flex items-center gap-2 ${getStatusColor(warning.status)}`}>
{getStatusIcon(warning.status)}
<span className="text-sm font-medium">{warning.status}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-3">
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-xs text-muted-foreground mb-1"></p>
<p className="text-sm font-medium flex items-center gap-1">
<User className="w-3 h-3" />
{warning.responsible}
</p>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-xs text-muted-foreground mb-1"></p>
<p className="text-sm">{warning.recipients.join(', ')}</p>
</div>
</div>
{/* 处置流程进度 */}
<div className="mb-3 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between text-xs mb-2">
<span className="text-muted-foreground"></span>
<span className="text-purple-600 font-medium">
{warning.timeline?.length || 0}
</span>
</div>
<div className="flex items-center gap-2">
<div className={`flex-1 h-2 rounded-full ${getProgressBgColor(warning.status)}`}>
<div
className={`h-full rounded-full transition-all ${getProgressColor(warning.status)}`}
style={{ width: `${getProgressPercentage(warning.status)}%` }}
/>
</div>
<span className="text-xs font-medium">{getProgressPercentage(warning.status)}%</span>
</div>
</div>
{warning.viewTime && (
<div className="p-3 bg-blue-50 rounded-lg mb-3">
<p className="text-xs text-blue-900 mb-1 flex items-center gap-1">
<Eye className="w-3 h-3" />
</p>
<p className="text-sm text-blue-800">{warning.viewTime}</p>
</div>
)}
{warning.disposalPlan && (
<div className="p-3 bg-orange-50 rounded-lg mb-3">
<p className="text-xs text-orange-900 mb-1 flex items-center gap-1">
<FileText className="w-3 h-3" />
</p>
<p className="text-sm text-orange-800 line-clamp-2">{warning.disposalPlan}</p>
{warning.disposalPlanSubmitTime && (
<p className="text-xs text-orange-600 mt-1">
: {warning.disposalPlanSubmitTime}
</p>
)}
</div>
)}
{warning.disposalResult && (
<div className="p-3 bg-green-50 rounded-lg mb-3">
<p className="text-xs text-green-900 mb-1 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3" />
</p>
<p className="text-sm text-green-800 line-clamp-2">{warning.disposalResult}</p>
<p className="text-xs text-green-700 mt-1">
: {warning.completedTime}
</p>
{warning.effectEvaluation && (
<div className="mt-2 pt-2 border-t border-green-200">
<p className="text-xs text-green-900 mb-1"></p>
<p className="text-sm text-green-800 line-clamp-1">{warning.effectEvaluation}</p>
</div>
)}
</div>
)}
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => onViewWarning(warning)}
>
<Eye className="w-4 h-4 mr-2" />
</Button>
{warning.timeline && warning.timeline.length > 0 && (
<Badge variant="outline" className="text-xs flex items-center gap-1">
<Clock className="w-3 h-3" />
{warning.timeline.length}
</Badge>
)}
</div>
</Card>
))}
</div>
</Card>
);
}

View File

@@ -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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* 基本信息 */}
<div className="p-4 bg-gray-50 rounded-lg">
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-sm font-medium mt-1">{selectedWarning.fieldName}</p>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xl">{getRiskTypeIcon(selectedWarning.riskType)}</span>
<span className="text-sm font-medium">{selectedWarning.riskType}</span>
</div>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<Badge className={`mt-1 ${getRiskLevelColor(selectedWarning.riskLevel)}`}>
{selectedWarning.riskLevel}
</Badge>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-sm font-medium mt-1">{selectedWarning.warningTime}</p>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-sm font-medium mt-1 flex items-center gap-1">
<User className="w-3 h-3" />
{selectedWarning.responsible}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full mt-1 ${getStatusColor(selectedWarning.status)}`}>
{getStatusIcon(selectedWarning.status)}
<span className="text-xs">{selectedWarning.status}</span>
</div>
</div>
</div>
</div>
<Tabs defaultValue="process" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="process"></TabsTrigger>
<TabsTrigger value="timeline">线</TabsTrigger>
<TabsTrigger value="details"></TabsTrigger>
</TabsList>
<TabsContent value="process" className="space-y-4 mt-4">
{/* 1. 查看确认 */}
<Card className={`p-4 ${selectedWarning.status !== '未查看' ? 'bg-blue-50' : ''}`}>
<div className="flex items-center gap-3 mb-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
selectedWarning.status !== '未查看' ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-600'
}`}>
1
</div>
<div>
<h4></h4>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
{selectedWarning.viewTime && (
<div className="ml-11 text-sm text-blue-600">
{selectedWarning.viewTime}
</div>
)}
{selectedWarning.status === '未查看' && (
<div className="ml-11">
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-700"
onClick={() => onUpdateStatus('已查看')}
>
<Eye className="w-4 h-4 mr-2" />
</Button>
</div>
)}
</Card>
{/* 2. 制定方案 */}
<Card className={`p-4 ${selectedWarning.status === '处理中' || selectedWarning.status === '已完成' ? 'bg-orange-50' : ''}`}>
<div className="flex items-center gap-3 mb-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
selectedWarning.status === '处理中' || selectedWarning.status === '已完成' ? 'bg-orange-600 text-white' : 'bg-gray-300 text-gray-600'
}`}>
2
</div>
<div>
<h4></h4>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
{selectedWarning.disposalPlan ? (
<div className="ml-11 space-y-2">
<div className="p-3 bg-white rounded border border-orange-200">
<p className="text-xs text-orange-900 mb-1"></p>
<p className="text-sm text-orange-800">{selectedWarning.disposalPlan}</p>
</div>
{selectedWarning.disposalPlanSubmitTime && (
<p className="text-xs text-orange-600">
{selectedWarning.disposalPlanSubmitTime}
</p>
)}
</div>
) : selectedWarning.status === '已查看' ? (
<div className="ml-11 space-y-3">
<div>
<label className="text-sm font-medium"></label>
<Textarea
value={disposalPlan}
onChange={(e) => onDisposalPlanChange(e.target.value)}
placeholder="请详细描述处置方案,包括具体措施、时间安排、人员分工等..."
rows={4}
className="mt-2"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm font-medium"></label>
<Textarea
value={resources}
onChange={(e) => onResourcesChange(e.target.value)}
placeholder="人员、设备、物资等..."
rows={2}
className="mt-2"
/>
</div>
<div>
<label className="text-sm font-medium"></label>
<Textarea
value={processingNotes}
onChange={(e) => onProcessingNotesChange(e.target.value)}
placeholder="现场处置过程记录..."
rows={2}
className="mt-2"
/>
</div>
</div>
<Button
className="bg-orange-600 hover:bg-orange-700"
onClick={() => onUpdateStatus('处理中')}
disabled={!disposalPlan.trim()}
>
<Activity className="w-4 h-4 mr-2" />
</Button>
</div>
) : null}
</Card>
{/* 3. 现场处置 */}
<Card className={`p-4 ${selectedWarning.status === '已完成' ? 'bg-green-50' : ''}`}>
<div className="flex items-center gap-3 mb-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
selectedWarning.status === '已完成' ? 'bg-green-600 text-white' : 'bg-gray-300 text-gray-600'
}`}>
3
</div>
<div>
<h4></h4>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
{selectedWarning.disposalResult ? (
<div className="ml-11 space-y-2">
<div className="p-3 bg-white rounded border border-green-200">
<p className="text-xs text-green-900 mb-1"></p>
<p className="text-sm text-green-800">{selectedWarning.disposalResult}</p>
</div>
{selectedWarning.actualCost && (
<div className="p-2 bg-white rounded border border-green-200">
<p className="text-xs text-green-900">{selectedWarning.actualCost}</p>
</div>
)}
{selectedWarning.effectEvaluation && (
<div className="p-3 bg-white rounded border border-green-200">
<p className="text-xs text-green-900 mb-1"></p>
<p className="text-sm text-green-800">{selectedWarning.effectEvaluation}</p>
</div>
)}
<p className="text-xs text-green-600">
{selectedWarning.completedTime}
</p>
</div>
) : selectedWarning.status === '处理中' ? (
<div className="ml-11 space-y-3">
<div>
<label className="text-sm font-medium"></label>
<Textarea
value={disposalResult}
onChange={(e) => onDisposalResultChange(e.target.value)}
placeholder="请详细描述处置结果、解决情况、当前作物状态等..."
rows={4}
className="mt-2"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm font-medium"></label>
<Textarea
value={actualCost}
onChange={(e) => onActualCostChange(e.target.value)}
placeholder="人工、材料等实际成本..."
rows={2}
className="mt-2"
/>
</div>
<div>
<label className="text-sm font-medium"></label>
<Textarea
value={effectEvaluation}
onChange={(e) => onEffectEvaluationChange(e.target.value)}
placeholder="处置效果、作物恢复情况等..."
rows={2}
className="mt-2"
/>
</div>
</div>
<Button
className="bg-green-600 hover:bg-green-700"
onClick={() => onUpdateStatus('已完成')}
disabled={!disposalResult.trim()}
>
<CheckCircle2 className="w-4 h-4 mr-2" />
</Button>
</div>
) : null}
</Card>
</TabsContent>
<TabsContent value="timeline" className="mt-4">
<Card className="p-4">
<h4 className="mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-purple-600" />
线
</h4>
{selectedWarning.timeline && selectedWarning.timeline.length > 0 ? (
<div className="space-y-4">
{selectedWarning.timeline.map((item, index) => (
<div key={index} className="flex gap-4">
<div className="flex flex-col items-center">
<div className={`w-3 h-3 rounded-full ${
index === 0 ? 'bg-red-500' :
index === selectedWarning.timeline!.length - 1 ? 'bg-green-500' :
'bg-blue-500'
}`} />
{index < selectedWarning.timeline!.length - 1 && (
<div className="w-0.5 h-12 bg-gray-300" />
)}
</div>
<div className="flex-1 pb-4">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium">{item.action}</span>
<Badge variant="outline" className="text-xs">{item.operator}</Badge>
</div>
<p className="text-xs text-muted-foreground mb-1">{item.time}</p>
{item.details && (
<p className="text-sm text-gray-700 mt-2 p-2 bg-gray-50 rounded">
{item.details}
</p>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-8">线</p>
)}
</Card>
</TabsContent>
<TabsContent value="details" className="mt-4">
<div className="grid grid-cols-2 gap-4">
{selectedWarning.processingNotes && (
<Card className="p-4 col-span-2">
<label className="text-sm font-medium"></label>
<div className="mt-2 p-3 bg-gray-50 rounded text-sm">
{selectedWarning.processingNotes}
</div>
</Card>
)}
{selectedWarning.resources && (
<Card className="p-4">
<label className="text-sm font-medium"></label>
<div className="mt-2 p-3 bg-gray-50 rounded text-sm">
{selectedWarning.resources}
</div>
</Card>
)}
{selectedWarning.actualCost && (
<Card className="p-4">
<label className="text-sm font-medium"></label>
<div className="mt-2 p-3 bg-gray-50 rounded text-sm">
{selectedWarning.actualCost}
</div>
</Card>
)}
<Card className="p-4">
<label className="text-sm font-medium"></label>
<div className="mt-2 p-3 bg-gray-50 rounded text-sm">
{selectedWarning.recipients.join(', ')}
</div>
</Card>
<Card className="p-4">
<label className="text-sm font-medium"></label>
<div className="mt-2 flex gap-1">
{selectedWarning.channels.map((ch, i) => (
<Badge key={i} variant="outline" className="text-xs">{ch}</Badge>
))}
</div>
</Card>
</div>
</TabsContent>
</Tabs>
<div className="flex gap-2 justify-end pt-4 border-t">
<Button variant="outline" onClick={onClose}>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,105 @@
'use client';
import { Card } from '@/components/ui/card';
import { CheckCircle2, Clock, FileText, Activity, CheckCheck, AlertTriangle } from 'lucide-react';
export function WorkflowGuide() {
return (
<Card className="p-6 bg-gradient-to-r from-green-50 to-blue-50 border-green-200">
<div className="space-y-4">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-6 h-6 text-green-600" />
<h3></h3>
</div>
<div className="bg-white rounded-lg p-4">
<p className="text-sm text-gray-700 mb-3"></p>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center text-sm">1</div>
<span className="text-sm"></span>
</div>
<span className="text-gray-400"></span>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center text-sm">2</div>
<span className="text-sm"></span>
</div>
<span className="text-gray-400"></span>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-orange-500 text-white flex items-center justify-center text-sm">3</div>
<span className="text-sm"></span>
</div>
<span className="text-gray-400"></span>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-purple-500 text-white flex items-center justify-center text-sm">4</div>
<span className="text-sm"></span>
</div>
<span className="text-gray-400"></span>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-green-500 text-white flex items-center justify-center text-sm">5</div>
<span className="text-sm"></span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-3">
<h4 className="flex items-center gap-2 mb-2">
<Clock className="w-4 h-4 text-blue-600" />
线
</h4>
<ul className="space-y-1 text-xs text-gray-600">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="bg-white rounded-lg p-3">
<h4 className="flex items-center gap-2 mb-2">
<FileText className="w-4 h-4 text-orange-600" />
</h4>
<ul className="space-y-1 text-xs text-gray-600">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="bg-white rounded-lg p-3">
<h4 className="flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-purple-600" />
</h4>
<ul className="space-y-1 text-xs text-gray-600">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="bg-white rounded-lg p-3">
<h4 className="flex items-center gap-2 mb-2">
<CheckCheck className="w-4 h-4 text-green-600" />
</h4>
<ul className="space-y-1 text-xs text-gray-600">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-xs text-yellow-900 flex items-start gap-2">
<AlertTriangle className="w-4 h-4 flex-shrink-0 mt-0.5" />
<span>
<strong></strong>
30
</span>
</p>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,523 @@
'use client';
import { useReducer, useEffect } from 'react';
import { toast } from 'sonner';
type RiskLevel = '无风险' | '轻度风险' | '中度风险' | '高度风险';
type RiskType = '干旱' | '涝渍' | '低温' | '高温' | '病虫害' | '强风';
type DisposalStatus = '未查看' | '已查看' | '处理中' | '已完成';
interface DisposalTimeline {
time: string;
action: string;
operator: string;
details?: string;
}
interface WarningRecord {
id: string;
fieldId: string;
fieldName: string;
riskType: RiskType;
riskLevel: RiskLevel;
warningTime: string;
recipients: string[];
channels: ('消息中心' | '短信' | '邮件')[];
status: DisposalStatus;
viewTime?: string;
disposalPlan?: string;
disposalPlanSubmitTime?: string;
disposalResult?: string;
completedTime?: string;
responsible: string;
timeline?: DisposalTimeline[];
processingNotes?: string;
resources?: string;
actualCost?: string;
effectEvaluation?: string;
}
// 状态接口
export interface DisposalState {
selectedField: string;
warningRecords: WarningRecord[];
selectedWarning: WarningRecord | null;
showWarningDetail: boolean;
disposalPlan: string;
disposalResult: string;
processingNotes: string;
resources: string;
actualCost: string;
effectEvaluation: string;
showRecordDetail: boolean;
selectedRecord: WarningRecord | null;
isInitializing: boolean;
}
// Action类型
export type DisposalAction =
| { type: 'SET_SELECTED_FIELD'; payload: string }
| { type: 'SET_WARNING_RECORDS'; payload: WarningRecord[] }
| { type: 'SET_SELECTED_WARNING'; payload: WarningRecord | null }
| { type: 'SET_SHOW_WARNING_DETAIL'; payload: boolean }
| { type: 'SET_DISPOSAL_PLAN'; payload: string }
| { type: 'SET_DISPOSAL_RESULT'; payload: string }
| { type: 'SET_PROCESSING_NOTES'; payload: string }
| { type: 'SET_RESOURCES'; payload: string }
| { type: 'SET_ACTUAL_COST'; payload: string }
| { type: 'SET_EFFECT_EVALUATION'; payload: string }
| { type: 'SET_SHOW_RECORD_DETAIL'; payload: boolean }
| { type: 'SET_SELECTED_RECORD'; payload: WarningRecord | null }
| { type: 'SET_INITIALIZING'; payload: boolean }
| { type: 'UPDATE_WARNING_STATUS'; payload: { id: string; status: DisposalStatus } }
| { type: 'UPDATE_WARNING_DISPOSAL'; payload: { id: string; updates: Partial<WarningRecord> } }
| { type: 'ADD_TIMELINE_ENTRY'; payload: { id: string; entry: DisposalTimeline } };
// 初始状态
const initialState: DisposalState = {
selectedField: 'all',
warningRecords: [],
selectedWarning: null,
showWarningDetail: false,
disposalPlan: '',
disposalResult: '',
processingNotes: '',
resources: '',
actualCost: '',
effectEvaluation: '',
showRecordDetail: false,
selectedRecord: null,
isInitializing: true,
};
// 模拟预警记录数据
export const mockWarningRecords: WarningRecord[] = [
{
id: 'warn-1',
fieldId: 'field-2',
fieldName: '西区2号地',
riskType: '涝渍',
riskLevel: '高度风险',
warningTime: '2024-10-15 14:33',
recipients: ['张三', '李四', '王五'],
channels: ['消息中心', '短信', '邮件'],
status: '处理中',
viewTime: '2024-10-15 14:35',
disposalPlan: '已启动排水系统,派遣工作组前往现场清理排水沟渠',
disposalPlanSubmitTime: '2024-10-15 14:40',
responsible: '张三',
processingNotes: '现场排水沟渠已清理完毕,正在使用水泵抽水',
resources: '水泵2台工作人员5人',
timeline: [
{ time: '2024-10-15 14:33', action: '预警发出', operator: '系统', details: '检测到涝渍风险等级达到高度风险' },
{ time: '2024-10-15 14:35', action: '责任人查看', operator: '张三', details: '已确认接收预警信息' },
{ time: '2024-10-15 14:40', action: '提交处置方案', operator: '张三', details: '启动排水系统,派遣工作组清理沟渠' },
{ time: '2024-10-15 14:45', action: '开始处置', operator: '张三', details: '工作组已到达现场,开始清理作业' },
],
},
{
id: 'warn-2',
fieldId: 'field-1',
fieldName: '东区1号地',
riskType: '干旱',
riskLevel: '中度风险',
warningTime: '2024-10-15 14:35',
recipients: ['张三', '李四'],
channels: ['消息中心', '短信'],
status: '已查看',
viewTime: '2024-10-15 14:36',
responsible: '李四',
timeline: [
{ time: '2024-10-15 14:35', action: '预警发出', operator: '系统', details: '检测到干旱风险等级达到中度风险' },
{ time: '2024-10-15 14:36', action: '责任人查看', operator: '李四', details: '已确认接收预警信息' },
],
},
{
id: 'warn-3',
fieldId: 'field-3',
fieldName: '南区3号地',
riskType: '低温',
riskLevel: '轻度风险',
warningTime: '2024-10-15 14:30',
recipients: ['王五'],
channels: ['消息中心'],
status: '已完成',
viewTime: '2024-10-15 14:31',
disposalPlan: '准备保温材料,延迟夜间灌溉',
disposalPlanSubmitTime: '2024-10-15 14:33',
disposalResult: '已完成保温材料准备,作物情况良好',
completedTime: '2024-10-15 15:20',
responsible: '王五',
processingNotes: '已覆盖保温膜,调整灌溉时间至白天进行',
resources: '保温膜200平方米人员3人',
actualCost: '1200元',
effectEvaluation: '处置及时有效,作物未受低温影响,长势良好',
timeline: [
{ time: '2024-10-15 14:30', action: '预警发出', operator: '系统', details: '检测到低温风险' },
{ time: '2024-10-15 14:31', action: '责任人查看', operator: '王五', details: '已确认接收预警' },
{ time: '2024-10-15 14:33', action: '提交处置方案', operator: '王五', details: '准备保温材料,调整灌溉计划' },
{ time: '2024-10-15 14:35', action: '开始处置', operator: '王五', details: '开始准备保温材料' },
{ time: '2024-10-15 15:20', action: '处置完成', operator: '王五', details: '保温措施实施完成,作物情况良好' },
],
},
{
id: 'warn-4',
fieldId: 'field-4',
fieldName: '北区4号地',
riskType: '病虫害',
riskLevel: '中度风险',
warningTime: '2024-10-15 14:28',
recipients: ['张三', '赵六'],
channels: ['消息中心', '短信', '邮件'],
status: '未查看',
responsible: '赵六',
timeline: [
{ time: '2024-10-15 14:28', action: '预警发出', operator: '系统', details: '检测到病虫害风险等级达到中度风险' },
],
},
];
// Reducer函数
export function disposalReducer(
state: DisposalState,
action: DisposalAction
): DisposalState {
switch (action.type) {
case 'SET_INITIALIZING':
return {
...state,
isInitializing: action.payload,
};
case 'SET_SELECTED_FIELD':
return {
...state,
selectedField: action.payload,
};
case 'SET_WARNING_RECORDS':
return {
...state,
warningRecords: action.payload,
};
case 'SET_SELECTED_WARNING':
return {
...state,
selectedWarning: action.payload,
};
case 'SET_SHOW_WARNING_DETAIL':
return {
...state,
showWarningDetail: action.payload,
};
case 'SET_DISPOSAL_PLAN':
return {
...state,
disposalPlan: action.payload,
};
case 'SET_DISPOSAL_RESULT':
return {
...state,
disposalResult: action.payload,
};
case 'SET_PROCESSING_NOTES':
return {
...state,
processingNotes: action.payload,
};
case 'SET_RESOURCES':
return {
...state,
resources: action.payload,
};
case 'SET_ACTUAL_COST':
return {
...state,
actualCost: action.payload,
};
case 'SET_EFFECT_EVALUATION':
return {
...state,
effectEvaluation: action.payload,
};
case 'SET_SHOW_RECORD_DETAIL':
return {
...state,
showRecordDetail: action.payload,
};
case 'SET_SELECTED_RECORD':
return {
...state,
selectedRecord: action.payload,
};
case 'UPDATE_WARNING_STATUS':
return {
...state,
warningRecords: state.warningRecords.map(record =>
record.id === action.payload.id
? { ...record, status: action.payload.status }
: record
),
};
case 'UPDATE_WARNING_DISPOSAL':
return {
...state,
warningRecords: state.warningRecords.map(record =>
record.id === action.payload.id
? { ...record, ...action.payload.updates }
: record
),
};
case 'ADD_TIMELINE_ENTRY':
return {
...state,
warningRecords: state.warningRecords.map(record =>
record.id === action.payload.id
? {
...record,
timeline: [...(record.timeline || []), action.payload.entry],
}
: record
),
};
default:
return state;
}
}
// 自定义Hook
export function useDisposalTracking() {
const [state, dispatch] = useReducer(disposalReducer, initialState);
// 初始化数据
useEffect(() => {
const initializeData = () => {
dispatch({ type: 'SET_INITIALIZING', payload: true });
dispatch({ type: 'SET_WARNING_RECORDS', payload: mockWarningRecords });
dispatch({ type: 'SET_INITIALIZING', payload: false });
};
initializeData();
}, []);
// 获取风险类型图标
const getRiskTypeIcon = (type: RiskType) => {
switch (type) {
case '干旱': return '☀️';
case '涝渍': return '🌧️';
case '低温': return '❄️';
case '高温': return '🌡️';
case '病虫害': return '🐛';
case '强风': return '💨';
default: return '⚠️';
}
};
// 获取状态图标
const getStatusIcon = (status: DisposalStatus) => {
switch (status) {
case '未查看': return '⭕';
case '已查看': return '👁️';
case '处理中': return '🔄';
case '已完成': return '✅';
default: return '⏰';
}
};
// 获取状态颜色
const getStatusColor = (status: DisposalStatus) => {
switch (status) {
case '未查看': return 'text-red-600 bg-red-50';
case '已查看': return 'text-blue-600 bg-blue-50';
case '处理中': return 'text-orange-600 bg-orange-50';
case '已完成': return 'text-green-600 bg-green-50';
default: return 'text-gray-600 bg-gray-50';
}
};
// 获取风险等级颜色
const getRiskLevelColor = (level: RiskLevel) => {
switch (level) {
case '无风险': return 'bg-gray-500 text-white';
case '轻度风险': return 'bg-yellow-500 text-white';
case '中度风险': return 'bg-orange-500 text-white';
case '高度风险': return 'bg-red-500 text-white';
default: return 'bg-gray-500 text-white';
}
};
// 查看预警详情
const handleViewWarning = (warning: WarningRecord) => {
dispatch({ type: 'SET_SELECTED_WARNING', payload: warning });
dispatch({ type: 'SET_DISPOSAL_PLAN', payload: warning.disposalPlan || '' });
dispatch({ type: 'SET_DISPOSAL_RESULT', payload: warning.disposalResult || '' });
dispatch({ type: 'SET_PROCESSING_NOTES', payload: warning.processingNotes || '' });
dispatch({ type: 'SET_RESOURCES', payload: warning.resources || '' });
dispatch({ type: 'SET_ACTUAL_COST', payload: warning.actualCost || '' });
dispatch({ type: 'SET_EFFECT_EVALUATION', payload: warning.effectEvaluation || '' });
dispatch({ type: 'SET_SHOW_WARNING_DETAIL', payload: true });
};
// 更新状态
const handleUpdateStatus = (status: DisposalStatus) => {
if (!state.selectedWarning) return;
const now = new Date().toLocaleString('zh-CN');
const timeline = [...(state.selectedWarning.timeline || [])];
if (status === '已查看' && !state.selectedWarning.viewTime) {
dispatch({
type: 'UPDATE_WARNING_DISPOSAL',
payload: {
id: state.selectedWarning.id,
updates: { viewTime: now, status }
}
});
timeline.push({
time: now,
action: '责任人查看',
operator: state.selectedWarning.responsible,
details: '已确认接收预警信息',
});
} else if (status === '处理中') {
dispatch({
type: 'UPDATE_WARNING_DISPOSAL',
payload: {
id: state.selectedWarning.id,
updates: {
disposalPlan: state.disposalPlan,
disposalPlanSubmitTime: now,
processingNotes: state.processingNotes,
resources: state.resources,
status
}
}
});
timeline.push({
time: now,
action: '提交处置方案',
operator: state.selectedWarning.responsible,
details: state.disposalPlan,
});
timeline.push({
time: now,
action: '开始处置',
operator: state.selectedWarning.responsible,
details: '开始执行处置方案',
});
} else if (status === '已完成') {
dispatch({
type: 'UPDATE_WARNING_DISPOSAL',
payload: {
id: state.selectedWarning.id,
updates: {
completedTime: now,
disposalResult: state.disposalResult,
actualCost: state.actualCost,
effectEvaluation: state.effectEvaluation,
status
}
}
});
timeline.push({
time: now,
action: '处置完成',
operator: state.selectedWarning.responsible,
details: state.disposalResult,
});
}
dispatch({
type: 'ADD_TIMELINE_ENTRY',
payload: { id: state.selectedWarning.id, entry: timeline[timeline.length - 1] }
});
// 更新选中的预警记录
const updatedWarning = {
...state.selectedWarning,
status,
timeline,
...(status === '已查看' && { viewTime: now }),
...(status === '处理中' && {
disposalPlan: state.disposalPlan,
disposalPlanSubmitTime: now,
processingNotes: state.processingNotes,
resources: state.resources,
}),
...(status === '已完成' && {
completedTime: now,
disposalResult: state.disposalResult,
actualCost: state.actualCost,
effectEvaluation: state.effectEvaluation,
}),
};
dispatch({ type: 'SET_SELECTED_WARNING', payload: updatedWarning });
toast.success(`预警状态已更新为:${status}`);
};
// 查看记录
const handleViewRecord = (record: WarningRecord) => {
dispatch({ type: 'SET_SELECTED_RECORD', payload: record });
dispatch({ type: 'SET_DISPOSAL_PLAN', payload: record.disposalPlan || '' });
dispatch({ type: 'SET_DISPOSAL_RESULT', payload: record.disposalResult || '' });
dispatch({ type: 'SET_SHOW_RECORD_DETAIL', payload: true });
// 如果是未查看状态,自动更新为已查看
if (record.status === '未查看') {
handleUpdateRecordStatus(record, '已查看');
}
};
// 更新记录状态
const handleUpdateRecordStatus = (record: WarningRecord, status: DisposalStatus) => {
const now = new Date().toLocaleString('zh-CN');
dispatch({
type: 'UPDATE_WARNING_DISPOSAL',
payload: {
id: record.id,
updates: { status, ...(status === '已查看' && !record.viewTime ? { viewTime: now } : {}) }
}
});
toast.success(`状态已更新为:${status}`);
};
// 保存记录处置
const handleSaveRecordDisposal = () => {
if (!state.selectedRecord) return;
dispatch({
type: 'UPDATE_WARNING_DISPOSAL',
payload: {
id: state.selectedRecord.id,
updates: {
disposalPlan: state.disposalPlan,
disposalResult: state.disposalResult,
status: state.disposalResult ? '已完成' : '处理中',
completedTime: state.disposalResult ? new Date().toLocaleString('zh-CN') : undefined,
}
}
});
dispatch({ type: 'SET_SHOW_RECORD_DETAIL', payload: false });
toast.success('处置信息已保存');
};
// 过滤后的预警记录
const filteredWarnings = state.selectedField === 'all'
? state.warningRecords
: state.warningRecords.filter(w => w.fieldId === state.selectedField);
return {
state,
dispatch,
filteredWarnings,
getRiskTypeIcon,
getStatusIcon,
getStatusColor,
getRiskLevelColor,
handleViewWarning,
handleUpdateStatus,
handleViewRecord,
handleUpdateRecordStatus,
handleSaveRecordDisposal,
};
}

View File

@@ -1,18 +1,107 @@
'use client'; 'use client';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { useDisposalTracking } from './components/disposalReducer';
import { FieldFilter } from './components/FieldFilter';
import { StatusStats } from './components/StatusStats';
import { WarningList } from './components/WarningList';
import { WorkflowDialog } from './components/WorkflowDialog';
import { WorkflowGuide } from './components/WorkflowGuide';
import { Activity } from 'lucide-react';
export default function DisposalPage() { export default function DisposalPage() {
const {
state,
dispatch,
filteredWarnings,
getRiskTypeIcon,
getStatusIcon,
getStatusColor,
getRiskLevelColor,
handleViewWarning,
handleUpdateStatus,
handleViewRecord,
handleUpdateRecordStatus,
handleSaveRecordDisposal,
} = useDisposalTracking();
if (state.isInitializing) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card className="p-6"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold"></h2> <div>
<div className="p-3 bg-muted rounded-lg mt-3"> <h2 className="text-green-800"></h2>
<p className="text-sm"> <p className="text-muted-foreground">
<strong></strong> /land-information/risk/disposal
</p> </p>
</div> </div>
</Card> </div>
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600 mx-auto mb-4"></div>
<p className="text-muted-foreground">...</p>
</div>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800"></h2>
<p className="text-muted-foreground">
</p>
</div>
</div>
{/* 地块筛选 */}
<FieldFilter
selectedField={state.selectedField}
onFieldChange={(value) => dispatch({ type: 'SET_SELECTED_FIELD', payload: value })}
/>
{/* 处置状态统计 */}
<StatusStats filteredWarnings={filteredWarnings} />
{/* 预警处置列表 */}
<WarningList
filteredWarnings={filteredWarnings}
getRiskTypeIcon={getRiskTypeIcon}
getRiskLevelColor={getRiskLevelColor}
getStatusIcon={getStatusIcon}
getStatusColor={getStatusColor}
onViewWarning={handleViewWarning}
/>
{/* 流程说明 */}
<WorkflowGuide />
{/* 工作流详情对话框 */}
<WorkflowDialog
isOpen={state.showWarningDetail}
onClose={() => dispatch({ type: 'SET_SHOW_WARNING_DETAIL', payload: false })}
selectedWarning={state.selectedWarning}
disposalPlan={state.disposalPlan}
disposalResult={state.disposalResult}
processingNotes={state.processingNotes}
resources={state.resources}
actualCost={state.actualCost}
effectEvaluation={state.effectEvaluation}
onDisposalPlanChange={(value) => dispatch({ type: 'SET_DISPOSAL_PLAN', payload: value })}
onDisposalResultChange={(value) => dispatch({ type: 'SET_DISPOSAL_RESULT', payload: value })}
onProcessingNotesChange={(value) => dispatch({ type: 'SET_PROCESSING_NOTES', payload: value })}
onResourcesChange={(value) => dispatch({ type: 'SET_RESOURCES', payload: value })}
onActualCostChange={(value) => dispatch({ type: 'SET_ACTUAL_COST', payload: value })}
onEffectEvaluationChange={(value) => dispatch({ type: 'SET_EFFECT_EVALUATION', payload: value })}
onUpdateStatus={handleUpdateStatus}
getRiskTypeIcon={getRiskTypeIcon}
getRiskLevelColor={getRiskLevelColor}
getStatusIcon={getStatusIcon}
getStatusColor={getStatusColor}
/>
</div> </div>
); );
} }

View File

@@ -1,17 +1,400 @@
'use client'; 'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
AlertTriangle,
CheckCircle2,
Clock,
CloudRain,
RefreshCw,
Snowflake,
Sun,
ThermometerSnowflake,
TrendingUp,
Wifi,
WifiOff,
Zap,
Bug,
Wind,
Activity,
MapPin,
AlertCircle,
} from 'lucide-react';
import { toast } from 'sonner';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
Area,
AreaChart,
} from 'recharts';
type RiskLevel = '无风险' | '轻度风险' | '中度风险' | '高度风险';
type RiskType = '干旱' | '涝渍' | '低温' | '高温' | '病虫害' | '强风';
interface RiskData {
id: string;
fieldId: string;
fieldName: string;
riskType: RiskType;
riskLevel: RiskLevel;
riskScore: number;
indexValue: number;
threshold: number;
updateTime: string;
sensorStatus: 'online' | 'offline';
location: string;
expectedImpact: string;
suggestedActions: string[];
}
export default function RiskMonitoringPage() { export default function RiskMonitoringPage() {
const [autoRefresh, setAutoRefresh] = useState(true);
const [selectedField, setSelectedField] = useState('all');
// 模拟实时风险数据
const [riskData, setRiskData] = useState<RiskData[]>([
{
id: 'risk-1',
fieldId: 'field-1',
fieldName: '东区1号地',
riskType: '干旱',
riskLevel: '中度风险',
riskScore: 65,
indexValue: 0.35,
threshold: 0.50,
updateTime: '2024-10-15 14:35',
sensorStatus: 'online',
location: '东经120.15°, 北纬30.25°',
expectedImpact: '预计未来3天无降雨土壤湿度将持续下降可能影响水稻生长',
suggestedActions: ['启动喷灌系统', '增加灌溉频次', '监测土壤湿度'],
},
{
id: 'risk-2',
fieldId: 'field-2',
fieldName: '西区2号地',
riskLevel: '高度风险',
riskType: '涝渍',
riskScore: 85,
indexValue: 0.82,
threshold: 0.70,
updateTime: '2024-10-15 14:33',
sensorStatus: 'online',
location: '东经120.12°, 北纬30.28°',
expectedImpact: '连续强降雨导致积水严重,可能造成玉米根系缺氧',
suggestedActions: ['开启排水系统', '清理排水沟渠', '加强巡查'],
},
{
id: 'risk-3',
fieldId: 'field-3',
fieldName: '南区3号地',
riskType: '低温',
riskLevel: '轻度风险',
riskScore: 45,
indexValue: 8.5,
threshold: 10.0,
updateTime: '2024-10-15 14:30',
sensorStatus: 'online',
location: '东经120.18°, 北纬30.22°',
expectedImpact: '夜间温度可能降至8℃以下需注意作物保温',
suggestedActions: ['准备保温材料', '关注天气预报', '适当延迟灌溉'],
},
{
id: 'risk-4',
fieldId: 'field-4',
fieldName: '北区4号地',
riskType: '病虫害',
riskLevel: '中度风险',
riskScore: 70,
indexValue: 68,
threshold: 50,
updateTime: '2024-10-15 14:28',
sensorStatus: 'offline',
location: '东经120.20°, 北纬30.30°',
expectedImpact: '温湿度适宜病虫害发生,需加强监测和防治',
suggestedActions: ['喷洒预防性农药', '设置诱虫灯', '加强田间巡查'],
},
]);
// 历史风险趋势数据
const riskTrendData = [
{ time: '10:00', 干旱指数: 0.25, 涝渍指数: 0.15, 温度: 22 },
{ time: '11:00', 干旱指数: 0.28, 涝渍指数: 0.20, 温度: 24 },
{ time: '12:00', 干旱指数: 0.30, 涝渍指数: 0.35, 温度: 26 },
{ time: '13:00', 干旱指数: 0.32, 涝渍指数: 0.55, 温度: 27 },
{ time: '14:00', 干旱指数: 0.35, 涝渍指数: 0.82, 温度: 25 },
];
const getRiskLevelColor = (level: RiskLevel) => {
switch (level) {
case '无风险': return 'bg-gray-500 text-white';
case '轻度风险': return 'bg-yellow-500 text-white';
case '中度风险': return 'bg-orange-500 text-white';
case '高度风险': return 'bg-red-500 text-white';
default: return 'bg-gray-500 text-white';
}
};
const getRiskTypeIcon = (type: RiskType) => {
switch (type) {
case '干旱': return <Sun className="w-5 h-5" />;
case '涝渍': return <CloudRain className="w-5 h-5" />;
case '低温': return <Snowflake className="w-5 h-5" />;
case '高温': return <ThermometerSnowflake className="w-5 h-5" />;
case '病虫害': return <Bug className="w-5 h-5" />;
case '强风': return <Wind className="w-5 h-5" />;
default: return <AlertTriangle className="w-5 h-5" />;
}
};
const handleRefresh = () => {
toast.success('风险数据已刷新');
};
const filteredRiskData = selectedField === 'all'
? riskData
: riskData.filter(r => r.fieldId === selectedField);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card className="p-6"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold"></h2> <div>
<div className="p-3 bg-muted rounded-lg mt-3"> <h2 className="text-green-800"></h2>
<p className="text-sm"> <p className="text-muted-foreground">
<strong></strong> /land-information/risk/monitoring
</p> </p>
</div> </div>
<div className="flex gap-2">
<div className="flex items-center gap-2 px-4 py-2 bg-card rounded-lg border">
<RefreshCw className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground"></span>
<Switch checked={autoRefresh} onCheckedChange={setAutoRefresh} />
</div>
<Button variant="outline" onClick={handleRefresh}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 地块筛选 */}
<Card className="p-4">
<div className="flex items-center gap-4">
<label className="text-sm font-medium">:</label>
<Select value={selectedField} onValueChange={setSelectedField}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="field-1">1</SelectItem>
<SelectItem value="field-2">西2</SelectItem>
<SelectItem value="field-3">3</SelectItem>
<SelectItem value="field-4">4</SelectItem>
</SelectContent>
</Select>
</div>
</Card>
{/* 风险统计 */}
<div className="grid grid-cols-4 gap-4">
<Card className="p-6 bg-gradient-to-br from-red-50 to-red-100 dark:from-red-950 dark:to-red-900">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-red-600 dark:text-red-400">
{filteredRiskData.filter(r => r.riskLevel === '高度风险').length}
</p>
</div>
<AlertTriangle className="w-12 h-12 text-red-600 dark:text-red-400 opacity-50" />
</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-950 dark:to-orange-900">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-orange-600 dark:text-orange-400">
{filteredRiskData.filter(r => r.riskLevel === '中度风险').length}
</p>
</div>
<AlertCircle className="w-12 h-12 text-orange-600 dark:text-orange-400 opacity-50" />
</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-yellow-50 to-yellow-100 dark:from-yellow-950 dark:to-yellow-900">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-yellow-600 dark:text-yellow-400">
{filteredRiskData.filter(r => r.riskLevel === '轻度风险').length}
</p>
</div>
<AlertTriangle className="w-12 h-12 text-yellow-600 dark:text-yellow-400 opacity-50" />
</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">线</p>
<p className="mt-2 text-3xl text-green-600 dark:text-green-400">
{filteredRiskData.filter(r => r.sensorStatus === 'online').length}/{filteredRiskData.length}
</p>
</div>
<Wifi className="w-12 h-12 text-green-600 dark:text-green-400 opacity-50" />
</div>
</Card>
</div>
{/* 风险监测列表 */}
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<Activity className="w-5 h-5 text-blue-600" />
</h3>
<div className="space-y-4">
{filteredRiskData.map(risk => (
<Card key={risk.id} className="p-4 border-l-4 hover:shadow-md transition-shadow" style={{
borderLeftColor:
risk.riskLevel === '高度风险' ? '#ef4444' :
risk.riskLevel === '中度风险' ? '#f97316' :
risk.riskLevel === '轻度风险' ? '#eab308' : '#6b7280'
}}>
<div className="flex items-start justify-between mb-3">
<div className="flex items-start gap-3">
<div className={`p-2 rounded-lg ${
risk.riskType === '干旱' ? 'bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-400' :
risk.riskType === '涝渍' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' :
risk.riskType === '低温' ? 'bg-cyan-100 text-cyan-600 dark:bg-cyan-900 dark:text-cyan-400' :
risk.riskType === '病虫害' ? 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-400' :
'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
}`}>
{getRiskTypeIcon(risk.riskType)}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<h4>{risk.fieldName}</h4>
<Badge className={getRiskLevelColor(risk.riskLevel)}>
{risk.riskLevel}
</Badge>
<Badge variant="outline" className="text-xs">
{risk.riskType}
</Badge>
{risk.sensorStatus === 'online' ? (
<Wifi className="w-4 h-4 text-green-500" />
) : (
<WifiOff className="w-4 h-4 text-red-500" />
)}
</div>
<p className="text-sm text-muted-foreground flex items-center gap-2">
<MapPin className="w-3 h-3" />
{risk.location}
</p>
</div>
</div>
<div className="text-right">
<p className="text-xs text-muted-foreground"></p>
<p className="text-2xl font-medium text-red-600">{risk.riskScore}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-3">
<div className="p-3 bg-muted rounded-lg">
<p className="text-xs text-muted-foreground mb-1"></p>
<div className="flex items-center gap-2">
<Progress value={(risk.indexValue / risk.threshold) * 100} className="flex-1" />
<span className="text-sm font-medium">
{risk.indexValue.toFixed(2)}
</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
: {risk.threshold.toFixed(2)}
</p>
</div>
<div className="p-3 bg-muted rounded-lg">
<p className="text-xs text-muted-foreground mb-1"></p>
<p className="text-sm font-medium flex items-center gap-1">
<Clock className="w-3 h-3" />
{risk.updateTime}
</p>
</div>
</div>
<div className="p-3 bg-orange-50 dark:bg-orange-950 border border-orange-200 dark:border-orange-800 rounded-lg mb-3">
<p className="text-xs text-orange-900 dark:text-orange-200 mb-1 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
</p>
<p className="text-sm text-orange-800 dark:text-orange-300">{risk.expectedImpact}</p>
</div>
<div className="p-3 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-xs text-green-900 dark:text-green-200 mb-2 flex items-center gap-1">
<Zap className="w-3 h-3" />
</p>
<ul className="space-y-1">
{risk.suggestedActions.map((action, index) => (
<li key={index} className="text-sm text-green-800 dark:text-green-300 flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>{action}</span>
</li>
))}
</ul>
</div>
</Card>
))}
</div>
</Card>
{/* 风险趋势图 */}
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-purple-600" />
</h3>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={riskTrendData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Legend />
<Area type="monotone" dataKey="干旱指数" stroke="#f97316" fill="#fed7aa" />
<Area type="monotone" dataKey="涝渍指数" stroke="#3b82f6" fill="#bfdbfe" />
</AreaChart>
</ResponsiveContainer>
</div>
</Card>
{/* 算法说明 */}
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-2">
<Zap className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-900 dark:text-blue-200">
<p className="mb-2"></p>
<ul className="space-y-1 text-xs">
<li> <strong></strong>: 湿&lt;0.5</li>
<li> <strong></strong>: &gt;0.7</li>
<li> <strong></strong>: &lt;10°C触发预警</li>
<li> <strong></strong>: 湿&gt;50</li>
<li> <strong></strong>: //</li>
</ul>
</div>
</div>
</Card> </Card>
</div> </div>
); );

View File

@@ -1,18 +1,678 @@
'use client'; 'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import {
AlertTriangle,
Bell,
CheckCircle2,
Eye,
MessageSquare,
Phone,
Mail,
Send,
AlertCircle,
Activity,
Clock,
CloudRain,
Sun,
Snowflake,
Bug,
Wind,
} from 'lucide-react';
import { toast } from 'sonner';
type RiskLevel = '无风险' | '轻度风险' | '中度风险' | '高度风险';
type RiskType = '干旱' | '涝渍' | '低温' | '高温' | '病虫害' | '强风';
type DisposalStatus = '未查看' | '已查看' | '处理中' | '已完成';
interface PushRule {
id: string;
name: string;
riskLevel: RiskLevel;
condition: string;
pushDelay: string;
recipients: string[];
channels: ('消息中心' | '短信' | '邮件')[];
enabled: boolean;
}
interface WarningRecord {
id: string;
fieldId: string;
fieldName: string;
riskType: RiskType;
riskLevel: RiskLevel;
warningTime: string;
recipients: string[];
channels: ('消息中心' | '短信' | '邮件')[];
status: DisposalStatus;
viewTime?: string;
disposalPlan?: string;
disposalPlanSubmitTime?: string;
disposalResult?: string;
completedTime?: string;
responsible: string;
}
export default function PushPage() { export default function PushPage() {
const [showRuleDialog, setShowRuleDialog] = useState(false);
const [showRecordDetail, setShowRecordDetail] = useState(false);
const [editingRule, setEditingRule] = useState<PushRule | null>(null);
const [selectedRecord, setSelectedRecord] = useState<WarningRecord | null>(null);
const [disposalPlan, setDisposalPlan] = useState('');
const [disposalResult, setDisposalResult] = useState('');
// 推送规则
const [pushRules, setPushRules] = useState<PushRule[]>([
{
id: 'rule-1',
name: '高度风险规则',
riskLevel: '高度风险',
condition: '风险评分 ≥ 80',
pushDelay: '立即推送',
recipients: ['地块责任人', '系统管理员'],
channels: ['消息中心', '短信', '邮件'],
enabled: true,
},
{
id: 'rule-2',
name: '中度风险规则',
riskLevel: '中度风险',
condition: '风险评分 ≥ 60',
pushDelay: '5分钟内推送',
recipients: ['地块责任人'],
channels: ['消息中心', '短信'],
enabled: true,
},
{
id: 'rule-3',
name: '轻度风险规则',
riskLevel: '轻度风险',
condition: '风险评分 ≥ 40',
pushDelay: '15分钟内推送',
recipients: ['地块责任人'],
channels: ['消息中心'],
enabled: true,
},
]);
// 预警记录
const [warningRecords] = useState<WarningRecord[]>([
{
id: 'warn-1',
fieldId: 'field-2',
fieldName: '西区2号地',
riskType: '涝渍',
riskLevel: '高度风险',
warningTime: '2024-10-15 14:33',
recipients: ['张三', '李四', '王五'],
channels: ['消息中心', '短信', '邮件'],
status: '处理中',
viewTime: '2024-10-15 14:35',
disposalPlan: '已启动排水系统,派遣工作组前往现场清理排水沟渠',
disposalPlanSubmitTime: '2024-10-15 14:40',
responsible: '张三',
},
{
id: 'warn-2',
fieldId: 'field-1',
fieldName: '东区1号地',
riskType: '干旱',
riskLevel: '中度风险',
warningTime: '2024-10-15 14:35',
recipients: ['张三', '李四'],
channels: ['消息中心', '短信'],
status: '已查看',
viewTime: '2024-10-15 14:36',
responsible: '李四',
},
{
id: 'warn-3',
fieldId: 'field-3',
fieldName: '南区3号地',
riskType: '低温',
riskLevel: '轻度风险',
warningTime: '2024-10-15 14:30',
recipients: ['王五'],
channels: ['消息中心'],
status: '已完成',
viewTime: '2024-10-15 14:31',
disposalPlan: '准备保温材料,延迟夜间灌溉',
disposalPlanSubmitTime: '2024-10-15 14:33',
disposalResult: '已完成保温材料准备,作物情况良好',
completedTime: '2024-10-15 15:20',
responsible: '王五',
},
{
id: 'warn-4',
fieldId: 'field-4',
fieldName: '北区4号地',
riskType: '病虫害',
riskLevel: '中度风险',
warningTime: '2024-10-15 14:28',
recipients: ['张三', '赵六'],
channels: ['消息中心', '短信', '邮件'],
status: '未查看',
responsible: '赵六',
},
]);
const getRiskLevelColor = (level: RiskLevel) => {
switch (level) {
case '无风险': return 'bg-gray-500 text-white';
case '轻度风险': return 'bg-yellow-500 text-white';
case '中度风险': return 'bg-orange-500 text-white';
case '高度风险': return 'bg-red-500 text-white';
default: return 'bg-gray-500 text-white';
}
};
const getRiskTypeIcon = (type: RiskType) => {
switch (type) {
case '干旱': return <Sun className="w-5 h-5" />;
case '涝渍': return <CloudRain className="w-5 h-5" />;
case '低温': return <Snowflake className="w-5 h-5" />;
case '高温': return <ThermometerSnowflake className="w-5 h-5" />;
case '病虫害': return <Bug className="w-5 h-5" />;
case '强风': return <Wind className="w-5 h-5" />;
default: return <AlertTriangle className="w-5 h-5" />;
}
};
const getStatusIcon = (status: DisposalStatus) => {
switch (status) {
case '未查看': return <AlertCircle className="w-4 h-4" />;
case '已查看': return <Eye className="w-4 h-4" />;
case '处理中': return <Activity className="w-4 h-4" />;
case '已完成': return <CheckCircle2 className="w-4 h-4" />;
default: return <Clock className="w-4 h-4" />;
}
};
const getStatusColor = (status: DisposalStatus) => {
switch (status) {
case '未查看': return 'text-red-600 bg-red-50';
case '已查看': return 'text-blue-600 bg-blue-50';
case '处理中': return 'text-orange-600 bg-orange-50';
case '已完成': return 'text-green-600 bg-green-50';
default: return 'text-gray-600 bg-gray-50';
}
};
// 推送规则管理
const handleAddRule = () => {
setEditingRule(null);
setShowRuleDialog(true);
};
const handleEditRule = (rule: PushRule) => {
setEditingRule(rule);
setShowRuleDialog(true);
};
const handleDeleteRule = (ruleId: string) => {
setPushRules(pushRules.filter(r => r.id !== ruleId));
toast.success('推送规则已删除');
};
const handleSaveRule = () => {
if (editingRule) {
// 编辑
setPushRules(pushRules.map(r => r.id === editingRule.id ? editingRule : r));
toast.success('推送规则已更新');
} else {
// 新增
const newRule: PushRule = {
id: `rule-${Date.now()}`,
name: '新规则',
riskLevel: '中度风险',
condition: '',
pushDelay: '立即推送',
recipients: [],
channels: ['消息中心'],
enabled: true,
};
setPushRules([...pushRules, newRule]);
toast.success('推送规则已创建');
}
setShowRuleDialog(false);
};
// 推送记录操作
const handleViewRecord = (record: WarningRecord) => {
setSelectedRecord(record);
setDisposalPlan(record.disposalPlan || '');
setDisposalResult(record.disposalResult || '');
setShowRecordDetail(true);
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card className="p-6"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold"></h2> <div>
<div className="p-3 bg-muted rounded-lg mt-3"> <h2 className="text-green-800"></h2>
<p className="text-sm"> <p className="text-muted-foreground">
<strong></strong> /land-information/risk/push
</p> </p>
</div> </div>
</div>
{/* 推送渠道配置 */}
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<Send className="w-5 h-5 text-green-600" />
</h3>
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<span className="font-medium"></span>
</div>
<Switch defaultChecked />
</div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-xs text-blue-600 dark:text-blue-400 mt-2"> </p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-950 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Phone className="w-5 h-5 text-green-600 dark:text-green-400" />
<span className="font-medium"></span>
</div>
<Switch defaultChecked />
</div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-xs text-green-600 dark:text-green-400 mt-2"> </p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Mail className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<span className="font-medium"></span>
</div>
<Switch defaultChecked />
</div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-xs text-purple-600 dark:text-purple-400 mt-2"> </p>
</div>
</div>
</Card> </Card>
{/* 推送规则 */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="flex items-center gap-2">
<Bell className="w-5 h-5 text-orange-600" />
</h3>
<Button onClick={handleAddRule} className="bg-green-600 hover:bg-green-700">
<Bell className="w-4 h-4 mr-2" />
</Button>
</div>
<div className="space-y-4">
{pushRules.map(rule => (
<div key={rule.id} className="p-4 bg-muted rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<h4 className="flex items-center gap-2">
{rule.riskLevel === '高度风险' && <AlertTriangle className="w-4 h-4 text-red-500" />}
{rule.riskLevel === '中度风险' && <AlertCircle className="w-4 h-4 text-orange-500" />}
{rule.riskLevel === '轻度风险' && <AlertTriangle className="w-4 h-4 text-yellow-500" />}
{rule.name}
</h4>
<Badge className={getRiskLevelColor(rule.riskLevel)}>
{rule.riskLevel}
</Badge>
<Badge variant="outline" className="text-xs">
{rule.pushDelay}
</Badge>
{!rule.enabled && (
<Badge variant="outline" className="text-xs text-gray-500">
</Badge>
)}
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => handleEditRule(rule)}>
</Button>
<Button
size="sm"
variant="outline"
className="text-red-600 hover:text-red-700"
onClick={() => handleDeleteRule(rule.id)}
>
</Button>
</div>
</div>
<p className="text-sm text-muted-foreground mb-2">
: {rule.condition}
</p>
<div className="grid grid-cols-2 gap-4">
<div className="text-sm">
<p className="text-xs text-muted-foreground mb-1">:</p>
<div className="flex gap-1 flex-wrap">
{rule.recipients.map((recipient, i) => (
<Badge key={i} variant="outline" className="text-xs">
{recipient}
</Badge>
))}
</div>
</div>
<div className="text-sm">
<p className="text-xs text-muted-foreground mb-1">:</p>
<div className="flex gap-1 flex-wrap">
{rule.channels.map((channel, i) => (
<Badge key={i} variant="outline" className="text-xs">
{channel}
</Badge>
))}
</div>
</div>
</div>
</div>
))}
</div>
</Card>
{/* 最近推送记录 */}
<Card className="p-6">
<h3 className="mb-4 flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-blue-600" />
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-4 py-3 text-left text-xs"></th>
<th className="px-4 py-3 text-left text-xs"></th>
<th className="px-4 py-3 text-left text-xs"></th>
<th className="px-4 py-3 text-center text-xs"></th>
<th className="px-4 py-3 text-left text-xs"></th>
<th className="px-4 py-3 text-left text-xs"></th>
<th className="px-4 py-3 text-center text-xs"></th>
<th className="px-4 py-3 text-center text-xs"></th>
</tr>
</thead>
<tbody>
{warningRecords.slice(0, 10).map(record => (
<tr key={record.id} className="border-t hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-4 py-3 text-sm">{record.warningTime}</td>
<td className="px-4 py-3 text-sm">{record.fieldName}</td>
<td className="px-4 py-3 text-sm">
<div className="flex items-center gap-2">
{getRiskTypeIcon(record.riskType)}
{record.riskType}
</div>
</td>
<td className="px-4 py-3 text-center">
<Badge className={getRiskLevelColor(record.riskLevel)}>
{record.riskLevel}
</Badge>
</td>
<td className="px-4 py-3">
<div className="flex gap-1">
{record.channels.map((channel, i) => (
<Badge key={i} variant="outline" className="text-xs">
{channel}
</Badge>
))}
</div>
</td>
<td className="px-4 py-3 text-sm">
{record.responsible}
</td>
<td className="px-4 py-3 text-center">
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full ${getStatusColor(record.status)}`}>
{getStatusIcon(record.status)}
<span className="text-xs">{record.status}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex gap-1 justify-center">
<Button
size="sm"
variant="outline"
onClick={() => handleViewRecord(record)}
>
<Eye className="w-3 h-3 mr-1" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
{/* 推送规则编辑对话框 */}
<Dialog open={showRuleDialog} onOpenChange={setShowRuleDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingRule ? '编辑推送规则' : '新增推送规则'}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium"></label>
<input
type="text"
className="mt-2 w-full px-3 py-2 border rounded-md bg-background"
placeholder="请输入规则名称"
defaultValue={editingRule?.name || ''}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium"></label>
<select
className="mt-2 w-full px-3 py-2 border rounded-md bg-background"
defaultValue={editingRule?.riskLevel || '中度风险'}
>
<option value="高度风险"></option>
<option value="中度风险"></option>
<option value="轻度风险"></option>
</select>
</div>
<div>
<label className="text-sm font-medium"></label>
<select
className="mt-2 w-full px-3 py-2 border rounded-md bg-background"
defaultValue={editingRule?.pushDelay || '立即推送'}
>
<option value="立即推送"></option>
<option value="5分钟内推送">5</option>
<option value="15分钟内推送">15</option>
<option value="30分钟内推送">30</option>
</select>
</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<select
className="mt-2 w-full px-3 py-2 border rounded-md bg-background"
defaultValue={editingRule?.condition || '风险评分 ≥ 60'}
>
<option value="风险评分 ≥ 80"> 80</option>
<option value="风险评分 ≥ 70"> 70</option>
<option value="风险评分 ≥ 60"> 60</option>
<option value="风险评分 ≥ 50"> 50</option>
<option value="风险评分 ≥ 40"> 40</option>
<option value="指数超阈值 50%以上"> 50%</option>
<option value="指数超阈值 30%以上"> 30%</option>
<option value="指数超阈值 20%以上"> 20%</option>
<option value="指数超阈值 10%以上"> 10%</option>
</select>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-2 space-y-2">
<div className="flex items-center gap-2">
<Checkbox id="recipient-1" defaultChecked />
<label htmlFor="recipient-1" className="text-sm cursor-pointer"></label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="recipient-2" />
<label htmlFor="recipient-2" className="text-sm cursor-pointer"></label>
</div>
</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-2 space-y-2">
<div className="flex items-center gap-2">
<Checkbox id="channel-1" defaultChecked />
<label htmlFor="channel-1" className="text-sm cursor-pointer"></label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="channel-2" defaultChecked />
<label htmlFor="channel-2" className="text-sm cursor-pointer"></label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="channel-3" />
<label htmlFor="channel-3" className="text-sm cursor-pointer"></label>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Checkbox id="enabled" defaultChecked={editingRule?.enabled ?? true} />
<label htmlFor="enabled" className="text-sm cursor-pointer"></label>
</div>
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => setShowRuleDialog(false)}>
</Button>
<Button
className="bg-green-600 hover:bg-green-700"
onClick={handleSaveRule}
>
<CheckCircle2 className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* 推送记录详情对话框 */}
<Dialog open={showRecordDetail} onOpenChange={setShowRecordDetail}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedRecord && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 text-sm">{selectedRecord.fieldName}</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 flex items-center gap-2 text-sm">
{getRiskTypeIcon(selectedRecord.riskType)}
{selectedRecord.riskType}
</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1">
<Badge className={getRiskLevelColor(selectedRecord.riskLevel)}>
{selectedRecord.riskLevel}
</Badge>
</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 text-sm">{selectedRecord.warningTime}</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 text-sm">{selectedRecord.responsible}</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1">
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full ${getStatusColor(selectedRecord.status)}`}>
{getStatusIcon(selectedRecord.status)}
<span className="text-xs">{selectedRecord.status}</span>
</div>
</div>
</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 flex gap-2">
{selectedRecord.channels.map((channel, i) => (
<Badge key={i} variant="outline">
{channel}
</Badge>
))}
</div>
</div>
{selectedRecord.viewTime && (
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 text-sm">{selectedRecord.viewTime}</div>
</div>
)}
{selectedRecord.disposalPlan && (
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 text-sm">{selectedRecord.disposalPlan}</div>
</div>
)}
{selectedRecord.disposalResult && (
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 text-sm">{selectedRecord.disposalResult}</div>
</div>
)}
{selectedRecord.completedTime && (
<div>
<label className="text-sm font-medium"></label>
<div className="mt-1 text-sm">{selectedRecord.completedTime}</div>
</div>
)}
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => setShowRecordDetail(false)}>
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div> </div>
); );
} }

View File

@@ -1,26 +1,459 @@
'use client'; 'use client';
import { useReducer, useEffect } from 'react'; import { useState } from 'react';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { spatialAnalysisReducer, initialSpatialAnalysisState, SpatialAnalysisState } from './components/spatialAnalysisReducer'; import { Button } from '@/components/ui/button';
import SpatialAnalysisContent from './components/SpatialAnalysisContent'; import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import {
Database,
Play,
Download,
Eye,
AlertTriangle,
CheckCircle2,
} from 'lucide-react';
import { toast } from 'sonner';
interface EvaluationFactor {
id: string;
name: string;
value: number;
weight: number;
unit: string;
optimalRange: [number, number];
score: number;
}
interface SuitabilityResult {
fieldId: string;
fieldName: string;
totalScore: number;
grade: '高度适宜' | '一般适宜' | '不适宜';
factors: EvaluationFactor[];
timestamp: string;
}
export default function BatchEvaluationPage() { export default function BatchEvaluationPage() {
const [state, dispatch] = useReducer(spatialAnalysisReducer, initialSpatialAnalysisState); const [isBatchRunning, setIsBatchRunning] = useState(false);
const [batchProgress, setBatchProgress] = useState(0);
const [batchResults, setBatchResults] = useState<{
total: number;
processed: number;
highSuitability: number;
mediumSuitability: number;
lowSuitability: number;
currentField: string;
}>({
total: 68,
processed: 0,
highSuitability: 0,
mediumSuitability: 0,
lowSuitability: 0,
currentField: '',
});
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
useEffect(() => { // 评价因子权重配置
// 加载存储的数据 const [factorWeights] = useState([
dispatch({ type: 'LOAD_FROM_STORAGE' }); { id: 'ph', name: 'pH值', weight: 20, unit: '' },
}, []); { id: 'organic', name: '有机质含量', weight: 25, unit: 'g/kg' },
{ id: 'depth', name: '土层厚度', weight: 20, unit: 'cm' },
{ id: 'nitrogen', name: '全氮', weight: 10, unit: 'g/kg' },
{ id: 'phosphorus', name: '全磷', weight: 10, unit: 'g/kg' },
{ id: 'potassium', name: '全钾', weight: 10, unit: 'g/kg' },
{ id: 'drainage', name: '排水性', weight: 5, unit: '' },
]);
useEffect(() => { const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
// 保存数据到localStorage
dispatch({ type: 'SAVE_TO_STORAGE' }); // 模拟从空间分析服务读取地块因子数据
}, [state.factors, state.weightConfig, state.analysisResults, state.batchTasks]); const fetchFieldFactorsFromSpatialService = (fieldId: string): EvaluationFactor[] => {
return [
{
id: 'ph',
name: 'pH值',
value: 6.0 + Math.random() * 2,
weight: factorWeights.find(f => f.id === 'ph')?.weight || 20,
unit: '',
optimalRange: [6.5, 7.5],
score: 0
},
{
id: 'organic',
name: '有机质含量',
value: 15 + Math.random() * 20,
weight: factorWeights.find(f => f.id === 'organic')?.weight || 25,
unit: 'g/kg',
optimalRange: [20, 30],
score: 0
},
{
id: 'depth',
name: '土层厚度',
value: 30 + Math.random() * 70,
weight: factorWeights.find(f => f.id === 'depth')?.weight || 20,
unit: 'cm',
optimalRange: [50, 80],
score: 0
},
{
id: 'nitrogen',
name: '全氮',
value: 0.8 + Math.random() * 1.5,
weight: factorWeights.find(f => f.id === 'nitrogen')?.weight || 10,
unit: 'g/kg',
optimalRange: [1.0, 2.0],
score: 0
},
{
id: 'phosphorus',
name: '全磷',
value: 0.4 + Math.random() * 1.2,
weight: factorWeights.find(f => f.id === 'phosphorus')?.weight || 10,
unit: 'g/kg',
optimalRange: [0.6, 1.2],
score: 0
},
{
id: 'potassium',
name: '全钾',
value: 12 + Math.random() * 13,
weight: factorWeights.find(f => f.id === 'potassium')?.weight || 10,
unit: 'g/kg',
optimalRange: [15, 20],
score: 0
},
{
id: 'drainage',
name: '排水性',
value: 60 + Math.random() * 40,
weight: factorWeights.find(f => f.id === 'drainage')?.weight || 5,
unit: '',
optimalRange: [70, 90],
score: 0
},
];
};
// 计算单个因子的得分
const calculateFactorScore = (value: number, optimalRange: [number, number]): number => {
const [min, max] = optimalRange;
const mid = (min + max) / 2;
const range = max - min;
if (value >= min && value <= max) {
const deviation = Math.abs(value - mid);
const deviationRatio = deviation / (range / 2);
return Math.max(85, 100 - deviationRatio * 15);
}
const deviation = value < min ? min - value : value - max;
const deviationRatio = deviation / range;
return Math.max(20, 85 - deviationRatio * 65);
};
// 计算综合评分
const calculateTotalScore = (factors: EvaluationFactor[]): number => {
let totalScore = 0;
for (const factor of factors) {
totalScore += (factor.score * factor.weight) / 100;
}
return Math.round(totalScore);
};
// 根据总分确定适宜性等级
const getGrade = (totalScore: number): '高度适宜' | '一般适宜' | '不适宜' => {
if (totalScore >= 80) return '高度适宜';
if (totalScore >= 60) return '一般适宜';
return '不适宜';
};
const getGradeColor = (grade: string) => {
switch (grade) {
case '高度适宜': return 'bg-green-500';
case '一般适宜': return 'bg-yellow-500';
case '不适宜': return 'bg-red-500';
default: return 'bg-gray-500';
}
};
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-green-600';
if (score >= 60) return 'text-yellow-600';
return 'text-red-600';
};
// 批量分析处理函数
const handleRunBatchAnalysis = async () => {
if (totalWeight !== 100) {
toast.error('权重总和必须为100%才能进行批量分析');
return;
}
setIsBatchRunning(true);
setBatchProgress(0);
setBatchResults({
total: 68,
processed: 0,
highSuitability: 0,
mediumSuitability: 0,
lowSuitability: 0,
currentField: '',
});
setBatchAnalysisResults([]);
toast.success('开始批量分析,正在读取地块数据...');
const totalFields = 68;
const results: SuitabilityResult[] = [];
for (let i = 0; i < totalFields; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
const fieldId = `field-${i + 1}`;
const fieldName = `地块${String.fromCharCode(65 + (i % 26))}${Math.floor(i / 26) + 1}`;
const factors = fetchFieldFactorsFromSpatialService(fieldId);
const scoredFactors = factors.map(factor => ({
...factor,
score: calculateFactorScore(factor.value, factor.optimalRange)
}));
const totalScore = calculateTotalScore(scoredFactors);
const grade = getGrade(totalScore);
const result: SuitabilityResult = {
fieldId,
fieldName,
totalScore,
grade,
factors: scoredFactors,
timestamp: new Date().toISOString(),
};
results.push(result);
setBatchResults(prev => ({
...prev,
processed: i + 1,
highSuitability: prev.highSuitability + (grade === '高度适宜' ? 1 : 0),
mediumSuitability: prev.mediumSuitability + (grade === '一般适宜' ? 1 : 0),
lowSuitability: prev.lowSuitability + (grade === '不适宜' ? 1 : 0),
currentField: fieldName,
}));
setBatchProgress(Math.round(((i + 1) / totalFields) * 100));
}
setBatchAnalysisResults(results);
setIsBatchRunning(false);
toast.success(`批量分析完成!已为${totalFields}个地块生成适宜性评价结果并更新到数据库`);
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<SpatialAnalysisContent state={state} dispatch={dispatch} /> <div className="flex items-center justify-between">
<div>
<h2 className="text-green-800"></h2>
<p className="text-muted-foreground">
</p>
</div>
</div>
<Card className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="mb-2"></h3>
<p className="text-sm text-muted-foreground">
</p>
</div>
<Button
size="lg"
className="bg-green-600 hover:bg-green-700"
onClick={handleRunBatchAnalysis}
disabled={isBatchRunning || totalWeight !== 100}
>
<Database className="w-5 h-5 mr-2" />
{isBatchRunning ? '分析中...' : '开始批量分析'}
</Button>
</div>
{totalWeight !== 100 && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
100%{totalWeight}%
</p>
</div>
)}
{isBatchRunning && (
<div className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">{batchProgress}%</span>
</div>
<Progress value={batchProgress} className="h-3" />
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="p-3 bg-blue-50 rounded-lg">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 text-blue-900">{batchResults.currentField}</p>
</div>
<div className="p-3 bg-blue-50 rounded-lg">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 text-blue-900">{batchResults.processed} / {batchResults.total} </p>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p> ...</p>
<p> pH...</p>
<p> ...</p>
<p> ...</p>
</div>
</div>
)}
{!isBatchRunning && batchResults.processed > 0 && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-sm text-green-800">
{batchResults.processed}
</p>
</div>
)}
</Card>
{/* 批量分析结果统计 */}
<div className="grid grid-cols-3 gap-4">
<Card className="p-6 bg-gradient-to-br from-green-50 to-green-100">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-green-600">{batchResults.highSuitability}</p>
<p className="text-xs text-muted-foreground mt-1">
({batchResults.total > 0 ? Math.round((batchResults.highSuitability / batchResults.total) * 100) : 0}%)
</p>
</div>
<CheckCircle2 className="w-12 h-12 text-green-600 opacity-50" />
</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-yellow-50 to-yellow-100">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-yellow-600">{batchResults.mediumSuitability}</p>
<p className="text-xs text-muted-foreground mt-1">
({batchResults.total > 0 ? Math.round((batchResults.mediumSuitability / batchResults.total) * 100) : 0}%)
</p>
</div>
<AlertTriangle className="w-12 h-12 text-yellow-600 opacity-50" />
</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-red-50 to-red-100">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="mt-2 text-3xl text-red-600">{batchResults.lowSuitability}</p>
<p className="text-xs text-muted-foreground mt-1">
({batchResults.total > 0 ? Math.round((batchResults.lowSuitability / batchResults.total) * 100) : 0}%)
</p>
</div>
<AlertTriangle className="w-12 h-12 text-red-600 opacity-50" />
</div>
</Card>
</div>
{/* 地块列表 */}
{batchAnalysisResults.length > 0 && (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3></h3>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<div className="overflow-x-auto max-h-[600px] overflow-y-auto">
<table className="w-full">
<thead className="bg-gray-50 sticky top-0">
<tr>
<th className="px-4 py-3 text-left text-xs"></th>
<th className="px-4 py-3 text-center text-xs"></th>
<th className="px-4 py-3 text-center text-xs"></th>
<th className="px-4 py-3 text-center text-xs">pH值</th>
<th className="px-4 py-3 text-center text-xs">(g/kg)</th>
<th className="px-4 py-3 text-center text-xs">(cm)</th>
<th className="px-4 py-3 text-center text-xs">(g/kg)</th>
<th className="px-4 py-3 text-center text-xs"></th>
<th className="px-4 py-3 text-center text-xs"></th>
</tr>
</thead>
<tbody>
{batchAnalysisResults.map((result) => (
<tr key={result.fieldId} className="border-t hover:bg-gray-50">
<td className="px-4 py-3 text-sm">{result.fieldName}</td>
<td className="px-4 py-3 text-center">
<span className={`text-sm font-medium ${getScoreColor(result.totalScore)}`}>
{result.totalScore}
</span>
</td>
<td className="px-4 py-3 text-center">
<Badge className={`${getGradeColor(result.grade)} text-white`}>
{result.grade}
</Badge>
</td>
<td className="px-4 py-3 text-center text-sm">
{result.factors.find(f => f.id === 'ph')?.value.toFixed(1)}
</td>
<td className="px-4 py-3 text-center text-sm">
{result.factors.find(f => f.id === 'organic')?.value.toFixed(1)}
</td>
<td className="px-4 py-3 text-center text-sm">
{result.factors.find(f => f.id === 'depth')?.value.toFixed(0)}
</td>
<td className="px-4 py-3 text-center text-sm">
{result.factors.find(f => f.id === 'nitrogen')?.value.toFixed(2)}
</td>
<td className="px-4 py-3 text-center text-sm text-muted-foreground">
{new Date(result.timestamp).toLocaleString('zh-CN')}
</td>
<td className="px-4 py-3 text-center">
<Button variant="outline" size="sm">
<Eye className="w-4 h-4" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
{/* 空间分析说明 */}
<Card className="p-4 bg-blue-50 border-blue-200">
<div className="flex items-start gap-2">
<Database className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800">
<p className="mb-2"></p>
<ul className="space-y-1 text-xs">
<li> <strong>1. </strong>: </li>
<li> <strong>2. </strong>: 7pH值</li>
<li> <strong>3. </strong>: 0-100</li>
<li> <strong>4. </strong>: 使=100% = Σ( × )</li>
<li> <strong>5. </strong>: (80)(60-79)(&lt;60)</li>
<li> <strong>6. </strong>: </li>
<li> <strong>7. </strong>: </li>
</ul>
</div>
</div>
</Card>
</div> </div>
); );
} }

View File

@@ -5,6 +5,7 @@ import {SideBarOld} from '@/components/layouts/SideBar/SideBarOld'
import '@/styles/globals.css' import '@/styles/globals.css'
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { Toaster } from '@/components/ui/sonner'
import { Building2, Users, Cog, Activity, Mail, UserCircle, Database, Map, BarChart3, Cloud, TrendingUp, GitCompare, AlertTriangle, FileText, MapPin, Settings, User, Package, Navigation, Zap, Target, PieChart, Calendar, Shield, Tractor, Clipboard, ClipboardCheck, Brain, Droplets, Book, ShoppingCart } from 'lucide-react' import { Building2, Users, Cog, Activity, Mail, UserCircle, Database, Map, BarChart3, Cloud, TrendingUp, GitCompare, AlertTriangle, FileText, MapPin, Settings, User, Package, Navigation, Zap, Target, PieChart, Calendar, Shield, Tractor, Clipboard, ClipboardCheck, Brain, Droplets, Book, ShoppingCart } from 'lucide-react'
const navbarData = { const navbarData = {
@@ -1330,6 +1331,7 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
<RootLayoutContent>{children}</RootLayoutContent> <RootLayoutContent>{children}</RootLayoutContent>
<Toaster />
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>