生产管理系统前端 - 地块风险预警开发
This commit is contained in:
@@ -1,78 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||
import { ReportGenerator } from './ReportGenerator';
|
||||
|
||||
interface FieldSelectorProps {
|
||||
fields: FieldData[];
|
||||
availableFields: ReturnType<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) {
|
||||
const { state, addField, removeField } = useChartAnalysis();
|
||||
|
||||
const availableFields = fields.filter(f => !state.selectedFields.includes(f.id));
|
||||
const comparisonFields = fields.filter(f => state.selectedFields.includes(f.id));
|
||||
|
||||
const handleAddField = (fieldId: string) => {
|
||||
const success = addField(fieldId);
|
||||
if (!success) {
|
||||
toast.error('最多只能同时对比4个地块');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveField = (fieldId: string) => {
|
||||
const success = removeField(fieldId);
|
||||
if (!success) {
|
||||
toast.error('至少需要选择2个地块进行对比');
|
||||
}
|
||||
};
|
||||
|
||||
export function FieldSelector({
|
||||
availableFields,
|
||||
comparisonFields,
|
||||
onAddField,
|
||||
onRemoveField,
|
||||
onGenerateReport,
|
||||
reportGenerating,
|
||||
reportProgress,
|
||||
}: FieldSelectorProps) {
|
||||
return (
|
||||
<Card className="p-4 bg-card">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-muted-foreground mb-2 block">选择对比地块 (2-4个)</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{comparisonFields.map(field => (
|
||||
<Badge
|
||||
key={field.id}
|
||||
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-3 py-1.5 flex items-center gap-2 font-light"
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-muted-foreground mb-2 block">选择对比地块 (2-4个)</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{comparisonFields.map(field => (
|
||||
<Badge
|
||||
key={field.id}
|
||||
className="bg-green-100 text-green-800 px-3 py-1.5 flex items-center gap-2 font-light"
|
||||
>
|
||||
{field.name}
|
||||
<button
|
||||
onClick={() => onRemoveField(field.id)}
|
||||
className="hover:bg-green-200 rounded-full p-0.5"
|
||||
>
|
||||
{field.name}
|
||||
<button
|
||||
onClick={() => handleRemoveField(field.id)}
|
||||
className="hover:bg-green-200 dark:hover:bg-green-800 rounded-full p-0.5"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
{availableFields.length > 0 && (
|
||||
<Select onValueChange={handleAddField}>
|
||||
<SelectTrigger className="w-40">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>添加地块</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableFields.map(field => (
|
||||
<SelectItem key={field.id} value={field.id}>
|
||||
{field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
{availableFields.length > 0 && (
|
||||
<Select onValueChange={onAddField}>
|
||||
<SelectTrigger className="w-40">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>添加地块</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableFields.map(field => (
|
||||
<SelectItem key={field.id} value={field.id}>
|
||||
{field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<ReportGenerator
|
||||
onGenerateReport={onGenerateReport}
|
||||
reportGenerating={reportGenerating}
|
||||
reportProgress={reportProgress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Map as MapIcon, MapPin } from 'lucide-react';
|
||||
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||
import { MapIcon, MapPin } from 'lucide-react';
|
||||
import { FieldData } from './chartComparisonReducer';
|
||||
|
||||
export function MapComparison() {
|
||||
const { state } = useChartAnalysis();
|
||||
|
||||
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
|
||||
interface MapComparisonProps {
|
||||
comparisonFields: FieldData[];
|
||||
}
|
||||
|
||||
export function MapComparison({ comparisonFields }: MapComparisonProps) {
|
||||
const getGradeColor = (grade: string) => {
|
||||
switch (grade) {
|
||||
case '高度适宜': return 'bg-green-500 text-white';
|
||||
@@ -19,33 +19,14 @@ export function MapComparison() {
|
||||
}
|
||||
};
|
||||
|
||||
// 如果没有选择地块,显示空状态
|
||||
if (comparisonFields.length === 0) {
|
||||
return (
|
||||
<Card className="p-6 bg-card">
|
||||
<h3 className="mb-4 flex items-center gap-2">
|
||||
<MapIcon className="w-5 h-5 text-blue-600" />
|
||||
地块空间分布对比
|
||||
</h3>
|
||||
<div className="h-64 flex items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<MapIcon className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>请先选择要对比的地块</p>
|
||||
<p className="text-sm mt-2">地图将显示各地块的空间分布位置</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 bg-card">
|
||||
<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="grid grid-cols-2 gap-4">
|
||||
{comparisonFields.map((field, index) => (
|
||||
{comparisonFields.map((field) => (
|
||||
<div key={field.id} className="space-y-2">
|
||||
<h4 className="text-sm font-medium">{field.name}</h4>
|
||||
<div className="h-48 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900 dark:to-green-800 rounded-lg flex items-center justify-center relative overflow-hidden">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Radar } from 'lucide-react';
|
||||
import {
|
||||
RadarChart,
|
||||
RadarChart as RechartsRadarChart,
|
||||
Radar as RechartsRadar,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
@@ -10,14 +11,13 @@ import {
|
||||
ResponsiveContainer,
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
import { Radar } from 'lucide-react';
|
||||
import { FieldData, useChartAnalysis } from './chartAnalysisReducer';
|
||||
import { FieldData } from './chartComparisonReducer';
|
||||
|
||||
export function ChartRadarAnalysis() {
|
||||
const { state } = useChartAnalysis();
|
||||
|
||||
const comparisonFields = state.fields.filter(f => state.selectedFields.includes(f.id));
|
||||
interface RadarChartProps {
|
||||
comparisonFields: FieldData[];
|
||||
}
|
||||
|
||||
export function RadarChart({ comparisonFields }: RadarChartProps) {
|
||||
// 雷达图数据
|
||||
const radarData = [
|
||||
{
|
||||
@@ -64,32 +64,8 @@ export function ChartRadarAnalysis() {
|
||||
},
|
||||
];
|
||||
|
||||
const colors = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'];
|
||||
|
||||
// 如果没有选择地块,显示空状态
|
||||
if (comparisonFields.length === 0) {
|
||||
return (
|
||||
<Card className="p-6 bg-card">
|
||||
<h3 className="mb-4 flex items-center gap-2">
|
||||
<Radar className="w-5 h-5 text-blue-600" />
|
||||
多维度雷达图对比
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
综合展示各地块在多个指标上的相对优劣势
|
||||
</p>
|
||||
<div className="h-96 flex items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Radar className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>请先选择要对比的地块</p>
|
||||
<p className="text-sm mt-2">雷达图将显示各地块的多维度指标对比</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 bg-card">
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-4 flex items-center gap-2">
|
||||
<Radar className="w-5 h-5 text-blue-600" />
|
||||
多维度雷达图对比
|
||||
@@ -99,22 +75,25 @@ export function ChartRadarAnalysis() {
|
||||
</p>
|
||||
<div className="h-96">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RadarChart data={radarData}>
|
||||
<RechartsRadarChart data={radarData}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="indicator" />
|
||||
<PolarRadiusAxis angle={90} domain={[0, 100]} />
|
||||
{comparisonFields.map((field, index) => (
|
||||
<RechartsRadar
|
||||
key={field.id}
|
||||
name={field.name}
|
||||
dataKey={field.name}
|
||||
stroke={colors[index]}
|
||||
fill={colors[index]}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
))}
|
||||
{comparisonFields.map((field, index) => {
|
||||
const colors = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'];
|
||||
return (
|
||||
<RechartsRadar
|
||||
key={field.id}
|
||||
name={field.name}
|
||||
dataKey={field.name}
|
||||
stroke={colors[index]}
|
||||
fill={colors[index]}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Legend />
|
||||
</RadarChart>
|
||||
</RechartsRadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,126 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { toast } from 'sonner';
|
||||
import { useChartComparison } from './components/chartComparisonReducer';
|
||||
import { FieldSelector } from './components/FieldSelector';
|
||||
import { ChartRadarAnalysis } from './components/RadarChart';
|
||||
import { YieldComparison } from './components/YieldComparison';
|
||||
import { NutrientComparison } from './components/NutrientComparison';
|
||||
import { RadarChart } from './components/RadarChart';
|
||||
import { YieldNutrientCharts } from './components/YieldNutrientCharts';
|
||||
import { MapComparison } from './components/MapComparison';
|
||||
import { useChartAnalysis, FieldData } from './components/chartAnalysisReducer';
|
||||
|
||||
// 模拟地块数据
|
||||
const mockFieldsData: FieldData[] = [
|
||||
{
|
||||
id: 'field-1',
|
||||
name: '东区1号地',
|
||||
area: 50.5,
|
||||
location: '东经120.15°, 北纬30.25°',
|
||||
soilType: '壤土',
|
||||
ph: 6.5,
|
||||
organicMatter: 32,
|
||||
nitrogen: 1.8,
|
||||
phosphorus: 1.2,
|
||||
potassium: 18,
|
||||
soilDepth: 85,
|
||||
slope: 3,
|
||||
currentCrop: '水稻',
|
||||
yield: 750,
|
||||
suitabilityScore: 87,
|
||||
suitabilityGrade: '高度适宜',
|
||||
irrigation: '喷灌',
|
||||
drainage: '良好',
|
||||
},
|
||||
{
|
||||
id: 'field-2',
|
||||
name: '西区2号地',
|
||||
area: 45.2,
|
||||
location: '东经120.12°, 北纬30.28°',
|
||||
soilType: '粘土',
|
||||
ph: 7.8,
|
||||
organicMatter: 22,
|
||||
nitrogen: 1.3,
|
||||
phosphorus: 0.9,
|
||||
potassium: 14,
|
||||
soilDepth: 55,
|
||||
slope: 5,
|
||||
currentCrop: '玉米',
|
||||
yield: 650,
|
||||
suitabilityScore: 72,
|
||||
suitabilityGrade: '一般适宜',
|
||||
irrigation: '滴灌',
|
||||
drainage: '中等',
|
||||
},
|
||||
{
|
||||
id: 'field-3',
|
||||
name: '南区3号地',
|
||||
area: 38.8,
|
||||
location: '东经120.18°, 北纬30.22°',
|
||||
soilType: '砂土',
|
||||
ph: 8.5,
|
||||
organicMatter: 15,
|
||||
nitrogen: 0.8,
|
||||
phosphorus: 0.6,
|
||||
potassium: 10,
|
||||
soilDepth: 42,
|
||||
slope: 8,
|
||||
currentCrop: '小麦',
|
||||
yield: 480,
|
||||
suitabilityScore: 58,
|
||||
suitabilityGrade: '不适宜',
|
||||
irrigation: '漫灌',
|
||||
drainage: '较差',
|
||||
},
|
||||
{
|
||||
id: 'field-4',
|
||||
name: '北区4号地',
|
||||
area: 55.0,
|
||||
location: '东经120.20°, 北纬30.30°',
|
||||
soilType: '壤土',
|
||||
ph: 6.8,
|
||||
organicMatter: 28,
|
||||
nitrogen: 1.6,
|
||||
phosphorus: 1.0,
|
||||
potassium: 16,
|
||||
soilDepth: 75,
|
||||
slope: 2,
|
||||
currentCrop: '大豆',
|
||||
yield: 380,
|
||||
suitabilityScore: 82,
|
||||
suitabilityGrade: '高度适宜',
|
||||
irrigation: '喷灌',
|
||||
drainage: '良好',
|
||||
},
|
||||
];
|
||||
import { ReportList } from './components/ReportList';
|
||||
|
||||
export default function ChartPage() {
|
||||
const { setFields, state } = useChartAnalysis();
|
||||
const {
|
||||
state,
|
||||
availableFields,
|
||||
comparisonFields,
|
||||
handleAddField,
|
||||
handleRemoveField,
|
||||
handleGenerateReport,
|
||||
handleDeleteReport,
|
||||
handleDownloadPDF,
|
||||
handleDownloadWord,
|
||||
} = useChartComparison();
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
// 直接使用模拟数据,确保数据可用
|
||||
setFields(mockFieldsData);
|
||||
localStorage.setItem('chart-analysis-fields', JSON.stringify(mockFieldsData));
|
||||
}, [setFields]);
|
||||
|
||||
// 如果数据还没有加载,显示loading状态
|
||||
if (state.fields.length === 0) {
|
||||
if (state.isInitializing) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800 dark:text-green-200">可视化图表分析</h2>
|
||||
<h2 className="text-green-800">可视化图表分析</h2>
|
||||
<p className="text-muted-foreground">
|
||||
多维度指标可视化展示与对比分析
|
||||
多维度指标看板、可视化图表分析与智能对比报告
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="p-6 bg-card">
|
||||
<div className="text-center py-8">
|
||||
<p>正在加载数据...</p>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -129,30 +46,74 @@ export default function ChartPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800 dark:text-green-200">可视化图表分析</h2>
|
||||
<h2 className="text-green-800">可视化图表分析</h2>
|
||||
<p className="text-muted-foreground">
|
||||
多维度指标可视化展示与对比分析
|
||||
多维度指标看板、可视化图表分析与智能对比报告
|
||||
</p>
|
||||
</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">
|
||||
{/* 雷达图 */}
|
||||
<ChartRadarAnalysis />
|
||||
<RadarChart comparisonFields={comparisonFields} />
|
||||
|
||||
{/* 产量与有机质对比 */}
|
||||
<YieldComparison />
|
||||
|
||||
{/* 养分对比 */}
|
||||
<NutrientComparison />
|
||||
{/* 产量与养分对比 */}
|
||||
<YieldNutrientCharts comparisonFields={comparisonFields} />
|
||||
|
||||
{/* 地图对比 */}
|
||||
<MapComparison />
|
||||
<MapComparison comparisonFields={comparisonFields} />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,18 +1,107 @@
|
||||
'use client';
|
||||
|
||||
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() {
|
||||
const {
|
||||
state,
|
||||
dispatch,
|
||||
filteredWarnings,
|
||||
getRiskTypeIcon,
|
||||
getStatusIcon,
|
||||
getStatusColor,
|
||||
getRiskLevelColor,
|
||||
handleViewWarning,
|
||||
handleUpdateStatus,
|
||||
handleViewRecord,
|
||||
handleUpdateRecordStatus,
|
||||
handleSaveRecordDisposal,
|
||||
} = useDisposalTracking();
|
||||
|
||||
if (state.isInitializing) {
|
||||
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>
|
||||
<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">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold">风险处置</h2>
|
||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
||||
<p className="text-sm">
|
||||
<strong>页面路径:</strong> /land-information/risk/disposal
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">预警处置跟踪</h2>
|
||||
<p className="text-muted-foreground">
|
||||
实时跟踪预警处置流程,形成闭环管理
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,400 @@
|
||||
'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 { 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() {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold">风险监测</h2>
|
||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
||||
<p className="text-sm">
|
||||
<strong>页面路径:</strong> /land-information/risk/monitoring
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">实时风险预测</h2>
|
||||
<p className="text-muted-foreground">
|
||||
实时监测地块风险,智能预警推送与处置跟踪
|
||||
</p>
|
||||
</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>: 基于土壤湿度、降雨量、蒸发量计算,指数<0.5触发预警</li>
|
||||
<li>• <strong>涝渍指数</strong>: 基于降雨强度、土壤排水性、地表积水深度,指数>0.7触发预警</li>
|
||||
<li>• <strong>低温强度</strong>: 基于气温、持续时长、作物耐寒性,温度<10°C触发预警</li>
|
||||
<li>• <strong>病虫害风险</strong>: 基于温湿度、历史数据、作物生长期,综合评分>50触发预警</li>
|
||||
<li>• <strong>风险等级</strong>: 根据指数偏离阈值程度自动分级(轻度/中度/高度)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,678 @@
|
||||
'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 { 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() {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold">风险推送</h2>
|
||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
||||
<p className="text-sm">
|
||||
<strong>页面路径:</strong> /land-information/risk/push
|
||||
<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">
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,459 @@
|
||||
'use client';
|
||||
|
||||
import { useReducer, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { spatialAnalysisReducer, initialSpatialAnalysisState, SpatialAnalysisState } from './components/spatialAnalysisReducer';
|
||||
import SpatialAnalysisContent from './components/SpatialAnalysisContent';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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() {
|
||||
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(() => {
|
||||
// 加载存储的数据
|
||||
dispatch({ type: 'LOAD_FROM_STORAGE' });
|
||||
}, []);
|
||||
// 评价因子权重配置
|
||||
const [factorWeights] = useState([
|
||||
{ id: 'ph', name: 'pH值', weight: 20, unit: '' },
|
||||
{ id: 'organic', name: '有机质含量', weight: 25, unit: 'g/kg' },
|
||||
{ id: 'depth', name: '土层厚度', weight: 20, unit: 'cm' },
|
||||
{ id: 'nitrogen', name: '全氮', weight: 10, unit: 'g/kg' },
|
||||
{ id: 'phosphorus', name: '全磷', weight: 10, unit: 'g/kg' },
|
||||
{ id: 'potassium', name: '全钾', weight: 10, unit: 'g/kg' },
|
||||
{ id: 'drainage', name: '排水性', weight: 5, unit: '' },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// 保存数据到localStorage
|
||||
dispatch({ type: 'SAVE_TO_STORAGE' });
|
||||
}, [state.factors, state.weightConfig, state.analysisResults, state.batchTasks]);
|
||||
const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
|
||||
|
||||
// 模拟从空间分析服务读取地块因子数据
|
||||
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 (
|
||||
<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>: 自动遍历每个地块,调用空间分析服务读取7项因子数据(pH值、有机质含量、土层厚度、全氮、全磷、全钾、排水性)</li>
|
||||
<li>• <strong>3. 因子评分</strong>: 根据实际值与最佳范围的接近程度,计算各因子得分(0-100分)</li>
|
||||
<li>• <strong>4. 加权汇总计算</strong>: 使用配置的权重体系(总和=100%),计算综合得分 = Σ(因子得分 × 因子权重)</li>
|
||||
<li>• <strong>5. 适宜性分级</strong>: 根据综合得分自动分级:高度适宜(≥80分)、一般适宜(60-79分)、不适宜(<60分)</li>
|
||||
<li>• <strong>6. 批量更新数据库</strong>: 将每个地块的适宜性指数和评价结果批量写回数据库,完成更新</li>
|
||||
<li>• <strong>7. 结果统计与可视化</strong>: 自动生成统计报告,支持按适宜性等级分类查看和导出</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {SideBarOld} from '@/components/layouts/SideBar/SideBarOld'
|
||||
import '@/styles/globals.css'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
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'
|
||||
|
||||
const navbarData = {
|
||||
@@ -1330,6 +1331,7 @@ export default function RootLayout({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<RootLayoutContent>{children}</RootLayoutContent>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user