生产管理系统前端 - 气象管理与环境监测提交
This commit is contained in:
@@ -0,0 +1,862 @@
|
|||||||
|
/**
|
||||||
|
* 土壤质量评价组件
|
||||||
|
* 提供土壤质量分析、评价和改良建议功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
BarChart3,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Activity,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
Info,
|
||||||
|
Droplets,
|
||||||
|
Leaf,
|
||||||
|
Zap,
|
||||||
|
Beaker,
|
||||||
|
Calculator,
|
||||||
|
FileText,
|
||||||
|
Calendar,
|
||||||
|
MapPin,
|
||||||
|
User,
|
||||||
|
Download,
|
||||||
|
RefreshCw,
|
||||||
|
Eye,
|
||||||
|
Settings,
|
||||||
|
Target,
|
||||||
|
Award,
|
||||||
|
AlertTriangle,
|
||||||
|
ThumbsUp
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
SoilQualityService,
|
||||||
|
SoilQualityEvaluation,
|
||||||
|
SoilIndicator,
|
||||||
|
SoilRecommendation,
|
||||||
|
SoilQualityHistory,
|
||||||
|
SoilAnalysisForm,
|
||||||
|
formatSoilScore,
|
||||||
|
getSoilGradeColor,
|
||||||
|
getIndicatorStatusColor,
|
||||||
|
formatDate
|
||||||
|
} from './soilQualityService';
|
||||||
|
import {
|
||||||
|
SOIL_TYPES,
|
||||||
|
SOIL_TEXTURES,
|
||||||
|
DRAINAGE_LEVELS
|
||||||
|
} from './soilTypes';
|
||||||
|
|
||||||
|
export function SoilQualityAnalysis() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'analysis' | 'evaluation' | 'history' | 'recommendations'>('analysis');
|
||||||
|
const [selectedField, setSelectedField] = useState('field-1');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [currentEvaluation, setCurrentEvaluation] = useState<SoilQualityEvaluation | null>(null);
|
||||||
|
const [historicalData, setHistoricalData] = useState<SoilQualityHistory[]>([]);
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const [formData, setFormData] = useState<SoilAnalysisForm>({
|
||||||
|
fieldId: 'field-1',
|
||||||
|
sampleDate: new Date().toISOString().split('T')[0],
|
||||||
|
sampleDepth: 20,
|
||||||
|
ph: 6.5,
|
||||||
|
organicMatter: 2.5,
|
||||||
|
nitrogen: 1.2,
|
||||||
|
phosphorus: 18,
|
||||||
|
potassium: 120,
|
||||||
|
calcium: 1200,
|
||||||
|
magnesium: 250,
|
||||||
|
sulfur: 15,
|
||||||
|
iron: 10,
|
||||||
|
manganese: 5,
|
||||||
|
zinc: 2,
|
||||||
|
copper: 1,
|
||||||
|
boron: 0.8,
|
||||||
|
soilTexture: 'medium',
|
||||||
|
drainage: 'good',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟地块数据
|
||||||
|
const mockFields = [
|
||||||
|
{ id: 'field-1', name: '东区1号地', code: 'DB001', area: 85.5 },
|
||||||
|
{ id: 'field-2', name: '西区2号地', code: 'DB002', area: 92.3 },
|
||||||
|
{ id: 'field-3', name: '南区3号地', code: 'DB003', area: 78.7 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
useEffect(() => {
|
||||||
|
loadHistoricalData();
|
||||||
|
}, [selectedField]);
|
||||||
|
|
||||||
|
const loadHistoricalData = () => {
|
||||||
|
const history = SoilQualityService.getHistoricalData(selectedField);
|
||||||
|
setHistoricalData(history);
|
||||||
|
|
||||||
|
// 如果有历史记录,显示最新的评价
|
||||||
|
if (history.length > 0) {
|
||||||
|
setCurrentEvaluation(history[0].evaluation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// 生成土壤质量评价
|
||||||
|
const evaluation = SoilQualityService.generateEvaluation(formData);
|
||||||
|
setCurrentEvaluation(evaluation);
|
||||||
|
|
||||||
|
// 更新历史数据
|
||||||
|
const newHistory: SoilQualityHistory = {
|
||||||
|
date: evaluation.date,
|
||||||
|
overallScore: evaluation.overallScore,
|
||||||
|
indicatorChanges: [],
|
||||||
|
evaluation
|
||||||
|
};
|
||||||
|
|
||||||
|
setHistoricalData([newHistory, ...historicalData]);
|
||||||
|
setShowForm(false);
|
||||||
|
setActiveTab('evaluation');
|
||||||
|
|
||||||
|
toast.success('土壤质量评价完成!');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('评价失败,请检查数据');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof SoilAnalysisForm, value: any) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportReport = () => {
|
||||||
|
if (!currentEvaluation) {
|
||||||
|
toast.error('没有可导出的评价报告');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟导出功能
|
||||||
|
toast.success('正在生成土壤质量评价报告...');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.success('报告已生成并下载');
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityColor = (priority: string): string => {
|
||||||
|
switch (priority) {
|
||||||
|
case 'high': return 'text-red-600 dark:text-red-400';
|
||||||
|
case 'medium': return 'text-yellow-600 dark:text-yellow-400';
|
||||||
|
case 'low': return 'text-green-600 dark:text-green-400';
|
||||||
|
default: return 'text-gray-600 dark:text-gray-400';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityBadgeVariant = (priority: string): string => {
|
||||||
|
switch (priority) {
|
||||||
|
case 'high': return 'destructive';
|
||||||
|
case 'medium': return 'default';
|
||||||
|
case 'low': return 'secondary';
|
||||||
|
default: return 'outline';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 页面头部 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-green-800 dark:text-green-200">土壤质量评价</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
专业的土壤质量分析与改良建议系统
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setShowForm(!showForm)}>
|
||||||
|
<Calculator className="w-4 h-4 mr-2" />
|
||||||
|
{showForm ? '隐藏表单' : '新建评价'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={loadHistoricalData}>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
刷新数据
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={exportReport} disabled={!currentEvaluation}>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出报告
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地块信息卡片 */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-green-50 to-blue-50 dark:from-green-950 dark:to-blue-950">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<MapPin className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground">当前地块</div>
|
||||||
|
<div className="font-medium">{mockFields.find(f => f.id === selectedField)?.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Award className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground">总体评分</div>
|
||||||
|
<div
|
||||||
|
className="font-bold text-lg"
|
||||||
|
style={{ color: currentEvaluation ? getSoilGradeColor(currentEvaluation.overallGrade) : '#6b7280' }}
|
||||||
|
>
|
||||||
|
{currentEvaluation ? formatSoilScore(currentEvaluation.overallScore) : '--'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Activity className="w-8 h-8 text-purple-600 dark:text-purple-400" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground">质量等级</div>
|
||||||
|
<div
|
||||||
|
className="font-bold text-lg"
|
||||||
|
style={{ color: currentEvaluation ? getSoilGradeColor(currentEvaluation.overallGrade) : '#6b7280' }}
|
||||||
|
>
|
||||||
|
{currentEvaluation ? currentEvaluation.overallGrade : '--'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<FileText className="w-8 h-8 text-orange-600 dark:text-orange-400" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground">历史记录</div>
|
||||||
|
<div className="font-medium">{historicalData.length} 条</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 土壤分析表单 */}
|
||||||
|
{showForm && (
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-semibold">土壤质量分析表单</h3>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setShowForm(false)}>
|
||||||
|
<Eye className="w-4 h-4 mr-2" />
|
||||||
|
查看评价结果
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-medium">基本信息</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="fieldId">地块选择</Label>
|
||||||
|
<Select value={formData.fieldId} onValueChange={(value) => handleInputChange('fieldId', value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{mockFields.map(field => (
|
||||||
|
<SelectItem key={field.id} value={field.id}>
|
||||||
|
{field.name} ({field.code})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="sampleDate">取样日期</Label>
|
||||||
|
<Input
|
||||||
|
id="sampleDate"
|
||||||
|
type="date"
|
||||||
|
value={formData.sampleDate}
|
||||||
|
onChange={(e) => handleInputChange('sampleDate', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="sampleDepth">取样深度 (cm)</Label>
|
||||||
|
<Input
|
||||||
|
id="sampleDepth"
|
||||||
|
type="number"
|
||||||
|
value={formData.sampleDepth}
|
||||||
|
onChange={(e) => handleInputChange('sampleDepth', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="soilTexture">土壤质地</Label>
|
||||||
|
<Select value={formData.soilTexture} onValueChange={(value) => handleInputChange('soilTexture', value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{SOIL_TEXTURES.map(texture => (
|
||||||
|
<SelectItem key={texture.id} value={texture.id}>
|
||||||
|
{texture.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="drainage">排水条件</Label>
|
||||||
|
<Select value={formData.drainage} onValueChange={(value) => handleInputChange('drainage', value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{DRAINAGE_LEVELS.map(level => (
|
||||||
|
<SelectItem key={level.id} value={level.id}>
|
||||||
|
{level.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 化学指标 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-medium">化学指标</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="ph">pH值</Label>
|
||||||
|
<Input
|
||||||
|
id="ph"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={formData.ph}
|
||||||
|
onChange={(e) => handleInputChange('ph', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="organicMatter">有机质 (%)</Label>
|
||||||
|
<Input
|
||||||
|
id="organicMatter"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={formData.organicMatter}
|
||||||
|
onChange={(e) => handleInputChange('organicMatter', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="nitrogen">全氮 (g/kg)</Label>
|
||||||
|
<Input
|
||||||
|
id="nitrogen"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={formData.nitrogen}
|
||||||
|
onChange={(e) => handleInputChange('nitrogen', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="phosphorus">有效磷 (mg/kg)</Label>
|
||||||
|
<Input
|
||||||
|
id="phosphorus"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={formData.phosphorus}
|
||||||
|
onChange={(e) => handleInputChange('phosphorus', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="potassium">速效钾 (mg/kg)</Label>
|
||||||
|
<Input
|
||||||
|
id="potassium"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={formData.potassium}
|
||||||
|
onChange={(e) => handleInputChange('potassium', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="notes">备注</Label>
|
||||||
|
<Textarea
|
||||||
|
id="notes"
|
||||||
|
value={formData.notes}
|
||||||
|
onChange={(e) => handleInputChange('notes', e.target.value)}
|
||||||
|
placeholder="请输入其他相关信息..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<Button onClick={handleFormSubmit} disabled={isLoading}>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
分析中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Calculator className="w-4 h-4 mr-2" />
|
||||||
|
生成评价
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as any)}>
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="analysis">指标分析</TabsTrigger>
|
||||||
|
<TabsTrigger value="evaluation">评价结果</TabsTrigger>
|
||||||
|
<TabsTrigger value="recommendations">改良建议</TabsTrigger>
|
||||||
|
<TabsTrigger value="history">历史记录</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 指标分析 */}
|
||||||
|
<TabsContent value="analysis" className="space-y-4">
|
||||||
|
{currentEvaluation ? (
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{/* 主要指标 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">主要指标得分</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentEvaluation.indicators
|
||||||
|
.filter(ind => ['ph', 'organicMatter', 'nitrogen', 'phosphorus', 'potassium'].includes(ind.id))
|
||||||
|
.map(indicator => (
|
||||||
|
<div key={indicator.id} className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: getIndicatorStatusColor(indicator.status) }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{indicator.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{indicator.value} {indicator.unit}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="font-bold">{indicator.score}分</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-light"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${getIndicatorStatusColor(indicator.status)}20`,
|
||||||
|
borderColor: getIndicatorStatusColor(indicator.status),
|
||||||
|
color: getIndicatorStatusColor(indicator.status)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{indicator.status === 'excellent' && '优秀'}
|
||||||
|
{indicator.status === 'good' && '良好'}
|
||||||
|
{indicator.status === 'moderate' && '中等'}
|
||||||
|
{indicator.status === 'poor' && '较差'}
|
||||||
|
{indicator.status === 'very_poor' && '很差'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 微量元素 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">微量元素得分</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentEvaluation.indicators
|
||||||
|
.filter(ind => ['iron', 'manganese', 'zinc', 'copper', 'boron'].includes(ind.id))
|
||||||
|
.map(indicator => (
|
||||||
|
<div key={indicator.id} className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: getIndicatorStatusColor(indicator.status) }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{indicator.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{indicator.value} {indicator.unit}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="font-bold">{indicator.score}分</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-light"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${getIndicatorStatusColor(indicator.status)}20`,
|
||||||
|
borderColor: getIndicatorStatusColor(indicator.status),
|
||||||
|
color: getIndicatorStatusColor(indicator.status)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{indicator.status === 'excellent' && '优秀'}
|
||||||
|
{indicator.status === 'good' && '良好'}
|
||||||
|
{indicator.status === 'moderate' && '中等'}
|
||||||
|
{indicator.status === 'poor' && '较差'}
|
||||||
|
{indicator.status === 'very_poor' && '很差'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-8 text-center bg-card">
|
||||||
|
<Beaker className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">暂无评价数据</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
请先填写土壤分析表单生成评价报告
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => setShowForm(true)}>
|
||||||
|
<Calculator className="w-4 h-4 mr-2" />
|
||||||
|
开始评价
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 评价结果 */}
|
||||||
|
<TabsContent value="evaluation" className="space-y-4">
|
||||||
|
{currentEvaluation ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 总体评价 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">总体评价</h3>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-center">
|
||||||
|
<div
|
||||||
|
className="text-4xl font-bold"
|
||||||
|
style={{ color: getSoilGradeColor(currentEvaluation.overallGrade) }}
|
||||||
|
>
|
||||||
|
{currentEvaluation.overallScore}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">综合得分</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div
|
||||||
|
className="text-3xl font-bold"
|
||||||
|
style={{ color: getSoilGradeColor(currentEvaluation.overallGrade) }}
|
||||||
|
>
|
||||||
|
{currentEvaluation.overallGrade}级
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">质量等级</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<Card className="p-4 bg-green-50 dark:bg-green-950">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Leaf className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
<span className="font-medium">土壤类型</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg text-green-600 dark:text-green-400">
|
||||||
|
{currentEvaluation.soilType}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Droplets className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<span className="font-medium">排水条件</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg text-blue-600 dark:text-blue-400">
|
||||||
|
{currentEvaluation.drainage === 'excellent' && '优秀'}
|
||||||
|
{currentEvaluation.drainage === 'good' && '良好'}
|
||||||
|
{currentEvaluation.drainage === 'moderate' && '中等'}
|
||||||
|
{currentEvaluation.drainage === 'poor' && '较差'}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4 bg-purple-50 dark:bg-purple-950">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Zap className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
<span className="font-medium">肥力水平</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg text-purple-600 dark:text-purple-400">
|
||||||
|
{currentEvaluation.fertility === 'high' && '高'}
|
||||||
|
{currentEvaluation.fertility === 'medium' && '中'}
|
||||||
|
{currentEvaluation.fertility === 'low' && '低'}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 评价信息 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mt-6">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Calendar className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">评价日期:</span>
|
||||||
|
<span>{formatDate(currentEvaluation.date)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<User className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">评价人员:</span>
|
||||||
|
<span>{currentEvaluation.evaluator}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<MapPin className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">地块位置:</span>
|
||||||
|
<span>{currentEvaluation.location}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Target className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">下次评价:</span>
|
||||||
|
<span>{formatDate(currentEvaluation.nextEvaluationDate)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentEvaluation.evaluationNotes && (
|
||||||
|
<div className="mt-4 p-3 bg-muted rounded-lg">
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">评价备注:</div>
|
||||||
|
<div className="text-sm">{currentEvaluation.evaluationNotes}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 得分分布 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">得分分布图</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{currentEvaluation.indicators.map(indicator => (
|
||||||
|
<div key={indicator.id} className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>{indicator.name}</span>
|
||||||
|
<span>{indicator.score}分</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded-full"
|
||||||
|
style={{
|
||||||
|
width: `${indicator.score}%`,
|
||||||
|
backgroundColor: getIndicatorStatusColor(indicator.status)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-8 text-center bg-card">
|
||||||
|
<Award className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">暂无评价结果</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
请先完成土壤质量分析以查看评价结果
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => setShowForm(true)}>
|
||||||
|
<Calculator className="w-4 h-4 mr-2" />
|
||||||
|
开始评价
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 改良建议 */}
|
||||||
|
<TabsContent value="recommendations" className="space-y-4">
|
||||||
|
{currentEvaluation && currentEvaluation.recommendations.length > 0 ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentEvaluation.recommendations.map((recommendation, index) => (
|
||||||
|
<Card key={index} className="p-6 bg-card">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{recommendation.priority === 'high' && <AlertTriangle className="w-5 h-5 text-red-500" />}
|
||||||
|
{recommendation.priority === 'medium' && <AlertCircle className="w-5 h-5 text-yellow-500" />}
|
||||||
|
{recommendation.priority === 'low' && <Info className="w-5 h-5 text-blue-500" />}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">{recommendation.title}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">{recommendation.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={getPriorityBadgeVariant(recommendation.priority) as any}>
|
||||||
|
{recommendation.priority === 'high' && '高优先级'}
|
||||||
|
{recommendation.priority === 'medium' && '中优先级'}
|
||||||
|
{recommendation.priority === 'low' && '低优先级'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 行动项目 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h5 className="font-medium mb-2">具体措施:</h5>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{recommendation.actionItems.map((item, itemIndex) => (
|
||||||
|
<li key={itemIndex} className="flex items-start gap-2 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 预期效果和成本 */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<Card className="p-3 bg-green-50 dark:bg-green-950">
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">预期改善</div>
|
||||||
|
<div className="font-bold text-green-600 dark:text-green-400">
|
||||||
|
+{recommendation.expectedImprovement}分
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-3 bg-blue-50 dark:bg-blue-950">
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">实施周期</div>
|
||||||
|
<div className="font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
{recommendation.timeframe}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-3 bg-purple-50 dark:bg-purple-950">
|
||||||
|
<div className="text-sm text-muted-foreground mb-1">预估成本</div>
|
||||||
|
<div className="font-bold text-purple-600 dark:text-purple-400">
|
||||||
|
¥{recommendation.estimatedCost}
|
||||||
|
{recommendation.costUnit === 'yuan_per_mu' && '/亩'}
|
||||||
|
{recommendation.costUnit === 'yuan_per_hectare' && '/公顷'}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-8 text-center bg-card">
|
||||||
|
<ThumbsUp className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">土壤质量良好</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
当前土壤质量状况良好,暂无特殊改良建议
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" onClick={() => setShowForm(true)}>
|
||||||
|
<Calculator className="w-4 h-4 mr-2" />
|
||||||
|
重新评价
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 历史记录 */}
|
||||||
|
<TabsContent value="history" className="space-y-4">
|
||||||
|
{historicalData.length > 0 ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 趋势分析 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<h3 className="mb-4">质量趋势分析</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm text-muted-foreground">总体趋势</span>
|
||||||
|
<span className={`font-medium ${
|
||||||
|
historicalData[0].overallScore > historicalData[1]?.overallScore ? 'text-green-600' :
|
||||||
|
historicalData[0].overallScore < historicalData[1]?.overallScore ? 'text-red-600' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{historicalData[0].overallScore > historicalData[1]?.overallScore && '↑ 上升趋势'}
|
||||||
|
{historicalData[0].overallScore < historicalData[1]?.overallScore && '↓ 下降趋势'}
|
||||||
|
{historicalData[0].overallScore === historicalData[1]?.overallScore && '→ 保持稳定'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
|
||||||
|
<div
|
||||||
|
className="h-3 rounded-full bg-gradient-to-r from-blue-500 to-green-500"
|
||||||
|
style={{ width: `${historicalData[0].overallScore}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||||
|
{historicalData[0].overallScore}分
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">最新评分</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 历史记录列表 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{historicalData.map((history, index) => (
|
||||||
|
<Card key={index} className="p-4 bg-card">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div
|
||||||
|
className="text-2xl font-bold"
|
||||||
|
style={{ color: getSoilGradeColor(history.evaluation.overallGrade) }}
|
||||||
|
>
|
||||||
|
{history.overallScore}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">得分</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">
|
||||||
|
{formatDate(history.date)} - {history.evaluation.overallGrade}级
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
评价人:{history.evaluation.evaluator}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{index === 0 && (
|
||||||
|
<Badge variant="default">最新</Badge>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Eye className="w-4 h-4 mr-1" />
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-8 text-center bg-card">
|
||||||
|
<FileText className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">暂无历史记录</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
完成第一次土壤质量评价后将显示历史记录
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => setShowForm(true)}>
|
||||||
|
<Calculator className="w-4 h-4 mr-2" />
|
||||||
|
开始评价
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* 使用说明 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Info className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="mb-2">土壤质量评价系统说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>科学评价</strong>:基于13项关键指标进行综合评分,包括pH值、有机质、氮磷钾、微量元素等</li>
|
||||||
|
<li>• <strong>智能分析</strong>:系统自动计算各指标得分,生成综合评价和质量等级</li>
|
||||||
|
<li>• <strong>改良建议</strong>:根据土壤状况提供针对性的改良措施和施肥建议</li>
|
||||||
|
<li>• <strong>历史追踪</strong>:记录历次评价结果,分析土壤质量变化趋势</li>
|
||||||
|
<li>• <strong>专业报告</strong>:支持导出详细的土壤质量评价报告,便于存档和分享</li>
|
||||||
|
<li>• <strong>成本估算</strong>:提供改良措施的预估成本和实施周期,帮助制定改良计划</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
/**
|
||||||
|
* 土壤质量评价服务类
|
||||||
|
* 提供土壤质量分析、评价和改良建议功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
SoilIndicator,
|
||||||
|
SoilQualityEvaluation,
|
||||||
|
SoilRecommendation,
|
||||||
|
SoilQualityHistory,
|
||||||
|
SoilAnalysisForm,
|
||||||
|
FertilizerRecommendation,
|
||||||
|
SOIL_INDICATORS,
|
||||||
|
SOIL_QUALITY_GRADES,
|
||||||
|
SOIL_TYPES,
|
||||||
|
SOIL_TEXTURES,
|
||||||
|
DRAINAGE_LEVELS
|
||||||
|
} from './soilTypes';
|
||||||
|
|
||||||
|
export class SoilQualityService {
|
||||||
|
/**
|
||||||
|
* 根据土壤分析表单数据生成土壤指标
|
||||||
|
*/
|
||||||
|
static generateIndicators(formData: SoilAnalysisForm): SoilIndicator[] {
|
||||||
|
const indicators: SoilIndicator[] = [];
|
||||||
|
|
||||||
|
// 处理每个指标
|
||||||
|
Object.entries(SOIL_INDICATORS).forEach(([key, config]) => {
|
||||||
|
let value: number;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'ph':
|
||||||
|
value = formData.ph;
|
||||||
|
break;
|
||||||
|
case 'organicMatter':
|
||||||
|
value = formData.organicMatter;
|
||||||
|
break;
|
||||||
|
case 'nitrogen':
|
||||||
|
value = formData.nitrogen;
|
||||||
|
break;
|
||||||
|
case 'phosphorus':
|
||||||
|
value = formData.phosphorus;
|
||||||
|
break;
|
||||||
|
case 'potassium':
|
||||||
|
value = formData.potassium;
|
||||||
|
break;
|
||||||
|
case 'calcium':
|
||||||
|
value = formData.calcium;
|
||||||
|
break;
|
||||||
|
case 'magnesium':
|
||||||
|
value = formData.magnesium;
|
||||||
|
break;
|
||||||
|
case 'sulfur':
|
||||||
|
value = formData.sulfur;
|
||||||
|
break;
|
||||||
|
case 'iron':
|
||||||
|
value = formData.iron;
|
||||||
|
break;
|
||||||
|
case 'manganese':
|
||||||
|
value = formData.manganese;
|
||||||
|
break;
|
||||||
|
case 'zinc':
|
||||||
|
value = formData.zinc;
|
||||||
|
break;
|
||||||
|
case 'copper':
|
||||||
|
value = formData.copper;
|
||||||
|
break;
|
||||||
|
case 'boron':
|
||||||
|
value = formData.boron;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const score = this.calculateIndicatorScore(value, config.optimalRange);
|
||||||
|
const status = this.getIndicatorStatus(score);
|
||||||
|
|
||||||
|
indicators.push({
|
||||||
|
id: config.id,
|
||||||
|
name: config.name,
|
||||||
|
value,
|
||||||
|
unit: config.unit,
|
||||||
|
optimalRange: config.optimalRange,
|
||||||
|
score,
|
||||||
|
status,
|
||||||
|
weight: config.weight,
|
||||||
|
description: config.description
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return indicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个指标的得分
|
||||||
|
*/
|
||||||
|
static calculateIndicatorScore(value: number, optimalRange: { min: number; max: number }): number {
|
||||||
|
if (value >= optimalRange.min && value <= optimalRange.max) {
|
||||||
|
// 在最佳范围内,满分
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const midPoint = (optimalRange.min + optimalRange.max) / 2;
|
||||||
|
const range = optimalRange.max - optimalRange.min;
|
||||||
|
|
||||||
|
if (value < optimalRange.min) {
|
||||||
|
// 低于最佳范围
|
||||||
|
const deviation = optimalRange.min - value;
|
||||||
|
if (deviation <= range * 0.5) {
|
||||||
|
return 100 - (deviation / (range * 0.5)) * 30;
|
||||||
|
} else {
|
||||||
|
return Math.max(0, 70 - (deviation - range * 0.5) / range * 70);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 高于最佳范围
|
||||||
|
const deviation = value - optimalRange.max;
|
||||||
|
if (deviation <= range * 0.5) {
|
||||||
|
return 100 - (deviation / (range * 0.5)) * 30;
|
||||||
|
} else {
|
||||||
|
return Math.max(0, 70 - (deviation - range * 0.5) / range * 70);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指标状态
|
||||||
|
*/
|
||||||
|
static getIndicatorStatus(score: number): 'excellent' | 'good' | 'moderate' | 'poor' | 'very_poor' {
|
||||||
|
if (score >= 90) return 'excellent';
|
||||||
|
if (score >= 80) return 'good';
|
||||||
|
if (score >= 70) return 'moderate';
|
||||||
|
if (score >= 60) return 'poor';
|
||||||
|
return 'very_poor';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算土壤质量总体评分
|
||||||
|
*/
|
||||||
|
static calculateOverallScore(indicators: SoilIndicator[]): number {
|
||||||
|
const weightedSum = indicators.reduce((sum, indicator) => {
|
||||||
|
return sum + (indicator.score * indicator.weight);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return Math.round(weightedSum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取土壤质量等级
|
||||||
|
*/
|
||||||
|
static getSoilQualityGrade(score: number): 'A' | 'B' | 'C' | 'D' | 'E' {
|
||||||
|
for (const [grade, config] of Object.entries(SOIL_QUALITY_GRADES)) {
|
||||||
|
if (score >= config.min && score <= config.max) {
|
||||||
|
return grade as 'A' | 'B' | 'C' | 'D' | 'E';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'E';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成土壤质量评价
|
||||||
|
*/
|
||||||
|
static generateEvaluation(formData: SoilAnalysisForm): SoilQualityEvaluation {
|
||||||
|
const indicators = this.generateIndicators(formData);
|
||||||
|
const overallScore = this.calculateOverallScore(indicators);
|
||||||
|
const overallGrade = this.getSoilQualityGrade(overallScore);
|
||||||
|
const recommendations = this.generateRecommendations(indicators, formData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `eval-${Date.now()}`,
|
||||||
|
fieldId: formData.fieldId,
|
||||||
|
fieldName: this.getFieldName(formData.fieldId),
|
||||||
|
date: formData.sampleDate,
|
||||||
|
location: this.getFieldLocation(formData.fieldId),
|
||||||
|
indicators,
|
||||||
|
overallScore,
|
||||||
|
overallGrade,
|
||||||
|
soilType: this.determineSoilType(formData.soilTexture),
|
||||||
|
texture: formData.soilTexture,
|
||||||
|
drainage: formData.drainage as any,
|
||||||
|
fertility: this.determineFertility(overallScore),
|
||||||
|
recommendations,
|
||||||
|
evaluationNotes: formData.notes || '',
|
||||||
|
evaluator: '系统自动评价',
|
||||||
|
nextEvaluationDate: this.calculateNextEvaluationDate(formData.sampleDate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成改良建议
|
||||||
|
*/
|
||||||
|
static generateRecommendations(indicators: SoilIndicator[], formData: SoilAnalysisForm): SoilRecommendation[] {
|
||||||
|
const recommendations: SoilRecommendation[] = [];
|
||||||
|
|
||||||
|
// 分析pH值
|
||||||
|
const phIndicator = indicators.find(ind => ind.id === 'ph');
|
||||||
|
if (phIndicator && (phIndicator.status === 'poor' || phIndicator.status === 'very_poor')) {
|
||||||
|
if (phIndicator.value < 6.0) {
|
||||||
|
recommendations.push({
|
||||||
|
category: 'ph_adjustment',
|
||||||
|
priority: 'high',
|
||||||
|
title: '土壤酸化改良',
|
||||||
|
description: '土壤偏酸性,需要进行酸化改良以提高pH值',
|
||||||
|
actionItems: [
|
||||||
|
'施用石灰,每亩施用量100-200公斤',
|
||||||
|
'增施有机肥,改善土壤缓冲能力',
|
||||||
|
'选择耐酸性作物进行轮作',
|
||||||
|
'减少酸性肥料的使用'
|
||||||
|
],
|
||||||
|
expectedImprovement: 15,
|
||||||
|
timeframe: '3-6个月',
|
||||||
|
estimatedCost: 200,
|
||||||
|
costUnit: 'yuan_per_mu'
|
||||||
|
});
|
||||||
|
} else if (phIndicator.value > 7.5) {
|
||||||
|
recommendations.push({
|
||||||
|
category: 'ph_adjustment',
|
||||||
|
priority: 'high',
|
||||||
|
title: '土壤碱化改良',
|
||||||
|
description: '土壤偏碱性,需要进行碱化改良以降低pH值',
|
||||||
|
actionItems: [
|
||||||
|
'施用硫磺或硫酸亚铁,每亩施用量20-50公斤',
|
||||||
|
'增施酸性有机肥(如松针堆肥)',
|
||||||
|
'种植绿肥作物,如三叶草、苜蓿',
|
||||||
|
'减少碱性肥料的使用'
|
||||||
|
],
|
||||||
|
expectedImprovement: 15,
|
||||||
|
timeframe: '6-12个月',
|
||||||
|
estimatedCost: 300,
|
||||||
|
costUnit: 'yuan_per_mu'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析有机质
|
||||||
|
const omIndicator = indicators.find(ind => ind.id === 'organicMatter');
|
||||||
|
if (omIndicator && omIndicator.status !== 'excellent') {
|
||||||
|
recommendations.push({
|
||||||
|
category: 'organic_matter',
|
||||||
|
priority: omIndicator.status === 'very_poor' ? 'high' : 'medium',
|
||||||
|
title: '有机质提升',
|
||||||
|
description: '土壤有机质含量偏低,需要增施有机肥',
|
||||||
|
actionItems: [
|
||||||
|
'施用腐熟农家肥,每亩2000-3000公斤',
|
||||||
|
'种植绿肥作物并翻压还田',
|
||||||
|
'秸秆还田,增加土壤有机质',
|
||||||
|
'使用生物有机肥,改善土壤微生物环境'
|
||||||
|
],
|
||||||
|
expectedImprovement: 20,
|
||||||
|
timeframe: '6-12个月',
|
||||||
|
estimatedCost: 500,
|
||||||
|
costUnit: 'yuan_per_mu'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析氮磷钾
|
||||||
|
const npkIndicators = indicators.filter(ind => ['nitrogen', 'phosphorus', 'potassium'].includes(ind.id));
|
||||||
|
const deficientNPK = npkIndicators.filter(ind => ind.status === 'poor' || ind.status === 'very_poor');
|
||||||
|
|
||||||
|
if (deficientNPK.length > 0) {
|
||||||
|
const fertilizerRec = this.generateFertilizerRecommendation(deficientNPK);
|
||||||
|
recommendations.push({
|
||||||
|
category: 'fertilizer',
|
||||||
|
priority: 'high',
|
||||||
|
title: '营养元素补充',
|
||||||
|
description: `土壤缺乏${deficientNPK.map(ind => ind.name).join('、')},需要针对性施肥`,
|
||||||
|
actionItems: fertilizerRec.actionItems,
|
||||||
|
expectedImprovement: 25,
|
||||||
|
timeframe: '1-3个月',
|
||||||
|
estimatedCost: fertilizerRec.estimatedCost,
|
||||||
|
costUnit: 'yuan_per_mu'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析微量元素
|
||||||
|
const microIndicators = indicators.filter(ind =>
|
||||||
|
['iron', 'manganese', 'zinc', 'copper', 'boron'].includes(ind.id) &&
|
||||||
|
(ind.status === 'poor' || ind.status === 'very_poor')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (microIndicators.length > 0) {
|
||||||
|
recommendations.push({
|
||||||
|
category: 'fertilizer',
|
||||||
|
priority: 'medium',
|
||||||
|
title: '微量元素补充',
|
||||||
|
description: `土壤缺乏${microIndicators.map(ind => ind.name).join('、')}等微量元素`,
|
||||||
|
actionItems: [
|
||||||
|
'施用复合微量元素肥料',
|
||||||
|
'叶面喷施微量元素溶液',
|
||||||
|
'增施有机肥,提高微量元素有效性',
|
||||||
|
'调节土壤pH值,改善微量元素吸收'
|
||||||
|
],
|
||||||
|
expectedImprovement: 10,
|
||||||
|
timeframe: '1-2个月',
|
||||||
|
estimatedCost: 150,
|
||||||
|
costUnit: 'yuan_per_mu'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排水改良建议
|
||||||
|
if (formData.drainage === 'poor' || formData.drainage === 'moderate') {
|
||||||
|
recommendations.push({
|
||||||
|
category: 'drainage',
|
||||||
|
priority: formData.drainage === 'poor' ? 'high' : 'medium',
|
||||||
|
title: '排水系统改良',
|
||||||
|
description: '土壤排水条件较差,需要改良排水系统',
|
||||||
|
actionItems: [
|
||||||
|
'开挖排水沟,改善地表排水',
|
||||||
|
'深耕土壤,打破犁底层',
|
||||||
|
'增施有机肥,改善土壤结构',
|
||||||
|
'种植深根系作物,改良土壤通透性'
|
||||||
|
],
|
||||||
|
expectedImprovement: 15,
|
||||||
|
timeframe: '3-6个月',
|
||||||
|
estimatedCost: 800,
|
||||||
|
costUnit: 'yuan_per_mu'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations.sort((a, b) => {
|
||||||
|
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||||
|
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成施肥建议
|
||||||
|
*/
|
||||||
|
private static generateFertilizerRecommendation(deficientIndicators: SoilIndicator[]): {
|
||||||
|
actionItems: string[];
|
||||||
|
estimatedCost: number;
|
||||||
|
} {
|
||||||
|
const actionItems: string[] = [];
|
||||||
|
let estimatedCost = 0;
|
||||||
|
|
||||||
|
deficientIndicators.forEach(indicator => {
|
||||||
|
switch (indicator.id) {
|
||||||
|
case 'nitrogen':
|
||||||
|
actionItems.push('施用尿素,每亩20-30公斤');
|
||||||
|
actionItems.push('或施用复合肥(15-15-15),每亩40-50公斤');
|
||||||
|
estimatedCost += 120;
|
||||||
|
break;
|
||||||
|
case 'phosphorus':
|
||||||
|
actionItems.push('施用过磷酸钙,每亩30-40公斤');
|
||||||
|
actionItems.push('或施用磷酸二铵,每亩15-20公斤');
|
||||||
|
estimatedCost += 100;
|
||||||
|
break;
|
||||||
|
case 'potassium':
|
||||||
|
actionItems.push('施用氯化钾,每亩10-15公斤');
|
||||||
|
actionItems.push('或施用硫酸钾,每亩12-18公斤');
|
||||||
|
estimatedCost += 80;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { actionItems, estimatedCost };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定土壤肥力等级
|
||||||
|
*/
|
||||||
|
static determineFertility(score: number): 'high' | 'medium' | 'low' {
|
||||||
|
if (score >= 80) return 'high';
|
||||||
|
if (score >= 60) return 'medium';
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定土壤类型
|
||||||
|
*/
|
||||||
|
static determineSoilType(texture: string): string {
|
||||||
|
const soilType = SOIL_TYPES.find(type => type.id === texture);
|
||||||
|
return soilType ? soilType.name : '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地块名称
|
||||||
|
*/
|
||||||
|
private static getFieldName(fieldId: string): string {
|
||||||
|
const mockFields = {
|
||||||
|
'field-1': '东区1号地',
|
||||||
|
'field-2': '西区2号地',
|
||||||
|
'field-3': '南区3号地'
|
||||||
|
};
|
||||||
|
return mockFields[fieldId as keyof typeof mockFields] || '未知地块';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地块位置
|
||||||
|
*/
|
||||||
|
private static getFieldLocation(fieldId: string): string {
|
||||||
|
const mockLocations = {
|
||||||
|
'field-1': '东区A区',
|
||||||
|
'field-2': '西区B区',
|
||||||
|
'field-3': '南区C区'
|
||||||
|
};
|
||||||
|
return mockLocations[fieldId as keyof typeof mockLocations] || '未知位置';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算下次评价日期
|
||||||
|
*/
|
||||||
|
private static calculateNextEvaluationDate(currentDate: string): string {
|
||||||
|
const date = new Date(currentDate);
|
||||||
|
date.setFullYear(date.getFullYear() + 1);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取历史记录数据
|
||||||
|
*/
|
||||||
|
static getHistoricalData(fieldId: string): SoilQualityHistory[] {
|
||||||
|
// 模拟历史数据
|
||||||
|
const mockHistory: SoilQualityHistory[] = [
|
||||||
|
{
|
||||||
|
date: '2024-01-15',
|
||||||
|
overallScore: 75,
|
||||||
|
indicatorChanges: [],
|
||||||
|
evaluation: {
|
||||||
|
id: 'eval-2024-01',
|
||||||
|
fieldId,
|
||||||
|
fieldName: this.getFieldName(fieldId),
|
||||||
|
date: '2024-01-15',
|
||||||
|
location: this.getFieldLocation(fieldId),
|
||||||
|
indicators: [],
|
||||||
|
overallScore: 75,
|
||||||
|
overallGrade: 'C',
|
||||||
|
soilType: '壤土',
|
||||||
|
texture: 'medium',
|
||||||
|
drainage: 'good',
|
||||||
|
fertility: 'medium',
|
||||||
|
recommendations: [],
|
||||||
|
evaluationNotes: '春季施肥前取样分析',
|
||||||
|
evaluator: '技术员张三',
|
||||||
|
nextEvaluationDate: '2025-01-15'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2023-07-20',
|
||||||
|
overallScore: 72,
|
||||||
|
indicatorChanges: [],
|
||||||
|
evaluation: {
|
||||||
|
id: 'eval-2023-07',
|
||||||
|
fieldId,
|
||||||
|
fieldName: this.getFieldName(fieldId),
|
||||||
|
date: '2023-07-20',
|
||||||
|
location: this.getFieldLocation(fieldId),
|
||||||
|
indicators: [],
|
||||||
|
overallScore: 72,
|
||||||
|
overallGrade: 'C',
|
||||||
|
soilType: '壤土',
|
||||||
|
texture: 'medium',
|
||||||
|
drainage: 'good',
|
||||||
|
fertility: 'medium',
|
||||||
|
recommendations: [],
|
||||||
|
evaluationNotes: '夏季作物生长期取样',
|
||||||
|
evaluator: '技术员李四',
|
||||||
|
nextEvaluationDate: '2024-07-20'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2023-01-10',
|
||||||
|
overallScore: 68,
|
||||||
|
indicatorChanges: [],
|
||||||
|
evaluation: {
|
||||||
|
id: 'eval-2023-01',
|
||||||
|
fieldId,
|
||||||
|
fieldName: this.getFieldName(fieldId),
|
||||||
|
date: '2023-01-10',
|
||||||
|
location: this.getFieldLocation(fieldId),
|
||||||
|
indicators: [],
|
||||||
|
overallScore: 68,
|
||||||
|
overallGrade: 'D',
|
||||||
|
soilType: '壤土',
|
||||||
|
texture: 'medium',
|
||||||
|
drainage: 'moderate',
|
||||||
|
fertility: 'low',
|
||||||
|
recommendations: [],
|
||||||
|
evaluationNotes: '年初土壤基础检测',
|
||||||
|
evaluator: '技术员王五',
|
||||||
|
nextEvaluationDate: '2024-01-10'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return mockHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析指标变化趋势
|
||||||
|
*/
|
||||||
|
static analyzeTrend(historicalData: SoilQualityHistory[]): {
|
||||||
|
trend: 'improving' | 'stable' | 'declining';
|
||||||
|
changeRate: number;
|
||||||
|
description: string;
|
||||||
|
} {
|
||||||
|
if (historicalData.length < 2) {
|
||||||
|
return {
|
||||||
|
trend: 'stable',
|
||||||
|
changeRate: 0,
|
||||||
|
description: '数据不足,无法分析趋势'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = historicalData[0];
|
||||||
|
const previous = historicalData[1];
|
||||||
|
const change = latest.overallScore - previous.overallScore;
|
||||||
|
|
||||||
|
let trend: 'improving' | 'stable' | 'declining';
|
||||||
|
let description: string;
|
||||||
|
|
||||||
|
if (Math.abs(change) < 3) {
|
||||||
|
trend = 'stable';
|
||||||
|
description = '土壤质量保持稳定';
|
||||||
|
} else if (change > 0) {
|
||||||
|
trend = 'improving';
|
||||||
|
description = `土壤质量正在改善,提升了${change}分`;
|
||||||
|
} else {
|
||||||
|
trend = 'declining';
|
||||||
|
description = `土壤质量有所下降,降低了${Math.abs(change)}分`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
trend,
|
||||||
|
changeRate: change,
|
||||||
|
description
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
export const formatSoilScore = (score: number): string => {
|
||||||
|
return `${score}分`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSoilGradeColor = (grade: string): string => {
|
||||||
|
const gradeConfig = SOIL_QUALITY_GRADES[grade as keyof typeof SOIL_QUALITY_GRADES];
|
||||||
|
return gradeConfig?.color || '#6b7280';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIndicatorStatusColor = (status: string): string => {
|
||||||
|
const colorMap = {
|
||||||
|
excellent: '#22c55e',
|
||||||
|
good: '#84cc16',
|
||||||
|
moderate: '#eab308',
|
||||||
|
poor: '#f97316',
|
||||||
|
very_poor: '#ef4444'
|
||||||
|
};
|
||||||
|
return colorMap[status as keyof typeof colorMap] || '#6b7280';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (dateString: string): string => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* 土壤质量评价类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SoilIndicator {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
unit: string;
|
||||||
|
optimalRange: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
score: number;
|
||||||
|
status: 'excellent' | 'good' | 'moderate' | 'poor' | 'very_poor';
|
||||||
|
weight: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoilQualityEvaluation {
|
||||||
|
id: string;
|
||||||
|
fieldId: string;
|
||||||
|
fieldName: string;
|
||||||
|
date: string;
|
||||||
|
location: string;
|
||||||
|
indicators: SoilIndicator[];
|
||||||
|
overallScore: number;
|
||||||
|
overallGrade: 'A' | 'B' | 'C' | 'D' | 'E';
|
||||||
|
soilType: string;
|
||||||
|
texture: string;
|
||||||
|
drainage: 'excellent' | 'good' | 'moderate' | 'poor';
|
||||||
|
fertility: 'high' | 'medium' | 'low';
|
||||||
|
recommendations: SoilRecommendation[];
|
||||||
|
evaluationNotes: string;
|
||||||
|
evaluator: string;
|
||||||
|
nextEvaluationDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoilRecommendation {
|
||||||
|
category: 'fertilizer' | 'organic_matter' | 'ph_adjustment' | 'drainage' | 'crop_rotation' | 'other';
|
||||||
|
priority: 'high' | 'medium' | 'low';
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
actionItems: string[];
|
||||||
|
expectedImprovement: number;
|
||||||
|
timeframe: string;
|
||||||
|
estimatedCost: number;
|
||||||
|
costUnit: 'yuan_per_mu' | 'yuan_per_hectare' | 'yuan_total';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoilQualityHistory {
|
||||||
|
date: string;
|
||||||
|
overallScore: number;
|
||||||
|
indicatorChanges: {
|
||||||
|
indicatorId: string;
|
||||||
|
indicatorName: string;
|
||||||
|
previousValue: number;
|
||||||
|
currentValue: number;
|
||||||
|
change: number;
|
||||||
|
trend: 'improving' | 'declining' | 'stable';
|
||||||
|
}[];
|
||||||
|
evaluation: SoilQualityEvaluation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoilQualityStandard {
|
||||||
|
region: string;
|
||||||
|
soilType: string;
|
||||||
|
indicators: {
|
||||||
|
[key: string]: {
|
||||||
|
optimalRange: { min: number; max: number };
|
||||||
|
acceptableRange: { min: number; max: number };
|
||||||
|
criticalRange: { min: number; max: number };
|
||||||
|
weight: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FertilizerRecommendation {
|
||||||
|
nutrientType: 'nitrogen' | 'phosphorus' | 'potassium' | 'organic_matter' | 'micronutrients';
|
||||||
|
currentLevel: number;
|
||||||
|
targetLevel: number;
|
||||||
|
deficit: number;
|
||||||
|
recommendedAmount: number;
|
||||||
|
unit: string;
|
||||||
|
fertilizerOptions: {
|
||||||
|
name: string;
|
||||||
|
content: number;
|
||||||
|
applicationRate: number;
|
||||||
|
cost: number;
|
||||||
|
timing: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoilAnalysisForm {
|
||||||
|
fieldId: string;
|
||||||
|
sampleDate: string;
|
||||||
|
sampleDepth: number;
|
||||||
|
ph: number;
|
||||||
|
organicMatter: number;
|
||||||
|
nitrogen: number;
|
||||||
|
phosphorus: number;
|
||||||
|
potassium: number;
|
||||||
|
calcium: number;
|
||||||
|
magnesium: number;
|
||||||
|
sulfur: number;
|
||||||
|
iron: number;
|
||||||
|
manganese: number;
|
||||||
|
zinc: number;
|
||||||
|
copper: number;
|
||||||
|
boron: number;
|
||||||
|
soilTexture: string;
|
||||||
|
drainage: string;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 土壤质量等级定义
|
||||||
|
export const SOIL_QUALITY_GRADES = {
|
||||||
|
A: { min: 90, max: 100, label: '优秀', color: '#22c55e', description: '土壤质量极佳,适合各种作物生长' },
|
||||||
|
B: { min: 80, max: 89, label: '良好', color: '#84cc16', description: '土壤质量良好,适合大部分作物生长' },
|
||||||
|
C: { min: 70, max: 79, label: '中等', color: '#eab308', description: '土壤质量中等,需要适当改良' },
|
||||||
|
D: { min: 60, max: 69, label: '较差', color: '#f97316', description: '土壤质量较差,需要重点改良' },
|
||||||
|
E: { min: 0, max: 59, label: '很差', color: '#ef4444', description: '土壤质量很差,需要全面改良' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 土壤指标定义
|
||||||
|
export const SOIL_INDICATORS = {
|
||||||
|
ph: {
|
||||||
|
id: 'ph',
|
||||||
|
name: 'pH值',
|
||||||
|
unit: '',
|
||||||
|
optimalRange: { min: 6.0, max: 7.5 },
|
||||||
|
weight: 0.15,
|
||||||
|
description: '土壤酸碱度,影响养分有效性和微生物活动'
|
||||||
|
},
|
||||||
|
organicMatter: {
|
||||||
|
id: 'organicMatter',
|
||||||
|
name: '有机质',
|
||||||
|
unit: '%',
|
||||||
|
optimalRange: { min: 2.5, max: 5.0 },
|
||||||
|
weight: 0.20,
|
||||||
|
description: '土壤有机质含量,反映土壤肥力和结构'
|
||||||
|
},
|
||||||
|
nitrogen: {
|
||||||
|
id: 'nitrogen',
|
||||||
|
name: '全氮',
|
||||||
|
unit: 'g/kg',
|
||||||
|
optimalRange: { min: 1.0, max: 2.0 },
|
||||||
|
weight: 0.15,
|
||||||
|
description: '土壤氮素含量,植物生长的重要营养元素'
|
||||||
|
},
|
||||||
|
phosphorus: {
|
||||||
|
id: 'phosphorus',
|
||||||
|
name: '有效磷',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 15, max: 30 },
|
||||||
|
weight: 0.15,
|
||||||
|
description: '土壤有效磷含量,促进植物根系发育和开花结果'
|
||||||
|
},
|
||||||
|
potassium: {
|
||||||
|
id: 'potassium',
|
||||||
|
name: '速效钾',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 100, max: 200 },
|
||||||
|
weight: 0.15,
|
||||||
|
description: '土壤速效钾含量,提高植物抗逆性和产量'
|
||||||
|
},
|
||||||
|
calcium: {
|
||||||
|
id: 'calcium',
|
||||||
|
name: '交换性钙',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 1000, max: 2000 },
|
||||||
|
weight: 0.05,
|
||||||
|
description: '土壤钙含量,调节土壤结构和植物细胞壁形成'
|
||||||
|
},
|
||||||
|
magnesium: {
|
||||||
|
id: 'magnesium',
|
||||||
|
name: '交换性镁',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 200, max: 400 },
|
||||||
|
weight: 0.05,
|
||||||
|
description: '土壤镁含量,叶绿素合成的重要元素'
|
||||||
|
},
|
||||||
|
sulfur: {
|
||||||
|
id: 'sulfur',
|
||||||
|
name: '有效硫',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 10, max: 30 },
|
||||||
|
weight: 0.05,
|
||||||
|
description: '土壤硫含量,蛋白质合成的重要元素'
|
||||||
|
},
|
||||||
|
iron: {
|
||||||
|
id: 'iron',
|
||||||
|
name: '有效铁',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 5, max: 20 },
|
||||||
|
weight: 0.025,
|
||||||
|
description: '土壤铁含量,叶绿素合成必需元素'
|
||||||
|
},
|
||||||
|
manganese: {
|
||||||
|
id: 'manganese',
|
||||||
|
name: '有效锰',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 2, max: 10 },
|
||||||
|
weight: 0.025,
|
||||||
|
description: '土壤锰含量,参与植物光合作用'
|
||||||
|
},
|
||||||
|
zinc: {
|
||||||
|
id: 'zinc',
|
||||||
|
name: '有效锌',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 1, max: 5 },
|
||||||
|
weight: 0.025,
|
||||||
|
description: '土壤锌含量,植物生长素合成的必需元素'
|
||||||
|
},
|
||||||
|
copper: {
|
||||||
|
id: 'copper',
|
||||||
|
name: '有效铜',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 0.5, max: 2 },
|
||||||
|
weight: 0.025,
|
||||||
|
description: '土壤铜含量,参与植物呼吸作用'
|
||||||
|
},
|
||||||
|
boron: {
|
||||||
|
id: 'boron',
|
||||||
|
name: '有效硼',
|
||||||
|
unit: 'mg/kg',
|
||||||
|
optimalRange: { min: 0.5, max: 2 },
|
||||||
|
weight: 0.025,
|
||||||
|
description: '土壤硼含量,植物开花结果必需元素'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 土壤类型定义
|
||||||
|
export const SOIL_TYPES = [
|
||||||
|
{ id: 'sandy_loam', name: '砂壤土', description: '排水良好,保肥能力中等' },
|
||||||
|
{ id: 'loam', name: '壤土', description: '质地适中,农业生产的理想土壤' },
|
||||||
|
{ id: 'clay_loam', name: '黏壤土', description: '保肥保水能力强,但排水较差' },
|
||||||
|
{ id: 'sandy', name: '砂土', description: '排水良好,但保肥能力差' },
|
||||||
|
{ id: 'clay', name: '黏土', description: '保肥保水能力强,通气性差' },
|
||||||
|
{ id: 'silt', name: '粉土', description: '肥力较高,但结构不稳定' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 土壤质地定义
|
||||||
|
export const SOIL_TEXTURES = [
|
||||||
|
{ id: 'coarse', name: '粗质地', description: '砂粒含量高,排水快' },
|
||||||
|
{ id: 'medium', name: '中等质地', description: '砂粒、粉粒、黏粒比例适中' },
|
||||||
|
{ id: 'fine', name: '细质地', description: '黏粒含量高,保水保肥能力强' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 排水等级定义
|
||||||
|
export const DRAINAGE_LEVELS = [
|
||||||
|
{ id: 'excellent', name: '优秀', description: '排水通畅,无积水现象' },
|
||||||
|
{ id: 'good', name: '良好', description: '排水较好,偶有短期积水' },
|
||||||
|
{ id: 'moderate', name: '中等', description: '排水一般,雨季有积水' },
|
||||||
|
{ id: 'poor', name: '较差', description: '排水困难,长期积水' }
|
||||||
|
];
|
||||||
@@ -1,18 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
import { SoilQualityAnalysis } from './components/SoilQualityAnalysis';
|
||||||
|
|
||||||
export default function SoilQualityPage() {
|
export default function SoilQualityPage() {
|
||||||
return (
|
return <SoilQualityAnalysis />;
|
||||||
<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/analysis/soil-quality
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||||
|
|
||||||
|
interface DataTrendsProps {
|
||||||
|
timeRange: '1h' | '6h' | '24h' | '7d';
|
||||||
|
onTimeRangeChange: (value: '1h' | '6h' | '24h' | '7d') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTrends({ timeRange, onTimeRangeChange }: DataTrendsProps) {
|
||||||
|
const environmentData = [
|
||||||
|
{ time: '00:00', temperature: 18, humidity: 75, co2: 380, light: 0, pm25: 25 },
|
||||||
|
{ time: '03:00', temperature: 16, humidity: 80, co2: 390, light: 0, pm25: 22 },
|
||||||
|
{ time: '06:00', temperature: 15, humidity: 85, co2: 400, light: 5000, pm25: 20 },
|
||||||
|
{ time: '09:00', temperature: 20, humidity: 70, co2: 410, light: 35000, pm25: 28 },
|
||||||
|
{ time: '12:00', temperature: 25, humidity: 55, co2: 420, light: 50000, pm25: 35 },
|
||||||
|
{ time: '15:00', temperature: 24.5, humidity: 62, co2: 420, light: 45000, pm25: 35 },
|
||||||
|
{ time: '18:00', temperature: 22, humidity: 65, co2: 410, light: 15000, pm25: 30 },
|
||||||
|
{ time: '21:00', temperature: 19, humidity: 72, co2: 395, light: 0, pm25: 26 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(['1h', '6h', '24h', '7d'] as const).map((range) => (
|
||||||
|
<button
|
||||||
|
key={range}
|
||||||
|
onClick={() => onTimeRangeChange(range)}
|
||||||
|
className={`px-3 py-1 rounded-lg text-sm transition-colors ${
|
||||||
|
timeRange === range
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{range === '1h' && '1小时'}
|
||||||
|
{range === '6h' && '6小时'}
|
||||||
|
{range === '24h' && '24小时'}
|
||||||
|
{range === '7d' && '7天'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">温度变化曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={environmentData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" className="dark:stroke-gray-600" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
label={{
|
||||||
|
value: '温度 (°C)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' },
|
||||||
|
className: "dark:fill-gray-400"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'var(--card)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value}°C`, '温度']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="temperature"
|
||||||
|
stroke="#dc2626"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#dc2626', r: 3 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">湿度变化曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={environmentData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" className="dark:stroke-gray-600" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
label={{
|
||||||
|
value: '湿度 (%)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' },
|
||||||
|
className: "dark:fill-gray-400"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'var(--card)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value}%`, '湿度']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="humidity"
|
||||||
|
stroke="#2563eb"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#2563eb', r: 3 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">CO₂浓度曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={environmentData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" className="dark:stroke-gray-600" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
label={{
|
||||||
|
value: 'CO₂ (ppm)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' },
|
||||||
|
className: "dark:fill-gray-400"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'var(--card)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value} ppm`, 'CO₂浓度']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="co2"
|
||||||
|
stroke="#22c55e"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#22c55e', r: 3 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">光照强度曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={environmentData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" className="dark:stroke-gray-600" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
className="dark:fill-gray-400"
|
||||||
|
label={{
|
||||||
|
value: '光照 (lux)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' },
|
||||||
|
className: "dark:fill-gray-400"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'var(--card)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value} lux`, '光照强度']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="light"
|
||||||
|
stroke="#eab308"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#eab308', r: 3 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Thermometer,
|
||||||
|
Droplets,
|
||||||
|
Leaf,
|
||||||
|
Eye,
|
||||||
|
Wind,
|
||||||
|
X
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SensorDevice, IoTDevice } from './environmentMonitoringReducer';
|
||||||
|
|
||||||
|
interface DeviceDialogProps {
|
||||||
|
showDeviceDialog: boolean;
|
||||||
|
editingDevice: SensorDevice | null;
|
||||||
|
deviceForm: {
|
||||||
|
iotDeviceId: string;
|
||||||
|
location: string;
|
||||||
|
fieldId: string;
|
||||||
|
};
|
||||||
|
availableIoTDevices: IoTDevice[];
|
||||||
|
onDeviceFormChange: (form: Partial<typeof deviceForm>) => void;
|
||||||
|
onSaveDevice: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceViewDialogProps {
|
||||||
|
showViewDialog: boolean;
|
||||||
|
viewingDevice: SensorDevice | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableFields = [
|
||||||
|
{ id: 'field-1', name: '东区1号地' },
|
||||||
|
{ id: 'field-2', name: '西区2号地' },
|
||||||
|
{ id: 'field-3', name: '南区3号地' },
|
||||||
|
{ id: 'field-4', name: '北区4号地' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DeviceDialog({
|
||||||
|
showDeviceDialog,
|
||||||
|
editingDevice,
|
||||||
|
deviceForm,
|
||||||
|
availableIoTDevices,
|
||||||
|
onDeviceFormChange,
|
||||||
|
onSaveDevice,
|
||||||
|
onCancel
|
||||||
|
}: DeviceDialogProps) {
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return 'bg-green-500';
|
||||||
|
case 'offline': return 'bg-gray-500';
|
||||||
|
case 'warning': return 'bg-yellow-500';
|
||||||
|
default: return 'bg-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return '在线';
|
||||||
|
case 'offline': return '离线';
|
||||||
|
case 'warning': return '警告';
|
||||||
|
default: return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showDeviceDialog} onOpenChange={onCancel}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingDevice ? '编辑设备' : '添加设备'}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{editingDevice ? '修改设备信息' : '从物联设备数据接入中选择设备并配置'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{!editingDevice && (
|
||||||
|
<div>
|
||||||
|
<Label>选择设备</Label>
|
||||||
|
<Select
|
||||||
|
value={deviceForm.iotDeviceId}
|
||||||
|
onValueChange={(value) => onDeviceFormChange({ iotDeviceId: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请从物联设备数据接入中选择" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableIoTDevices.length === 0 ? (
|
||||||
|
<div className="p-4 text-sm text-muted-foreground text-center">
|
||||||
|
暂无可用设备<br />
|
||||||
|
请先在AI系统-全域数据感知中心-物联设备数据接入中添加环境监测站设备
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
availableIoTDevices.map((device) => (
|
||||||
|
<SelectItem key={device.id} value={device.id}>
|
||||||
|
{device.code} - {device.name} ({device.manufacturer} {device.model})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>选择地块</Label>
|
||||||
|
<Select
|
||||||
|
value={deviceForm.fieldId}
|
||||||
|
onValueChange={(value) => onDeviceFormChange({ fieldId: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择地块" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableFields.map((field) => (
|
||||||
|
<SelectItem key={field.id} value={field.id}>
|
||||||
|
{field.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>安装位置</Label>
|
||||||
|
<Input
|
||||||
|
value={deviceForm.location}
|
||||||
|
onChange={(e) => onDeviceFormChange({ location: e.target.value })}
|
||||||
|
placeholder="如:地块中心位置"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||||
|
<Button variant="outline" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-green-600 hover:bg-green-700" onClick={onSaveDevice}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceViewDialog({
|
||||||
|
showViewDialog,
|
||||||
|
viewingDevice,
|
||||||
|
onClose
|
||||||
|
}: DeviceViewDialogProps) {
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return 'bg-green-500';
|
||||||
|
case 'offline': return 'bg-gray-500';
|
||||||
|
case 'warning': return 'bg-yellow-500';
|
||||||
|
default: return 'bg-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return '在线';
|
||||||
|
case 'offline': return '离线';
|
||||||
|
case 'warning': return '警告';
|
||||||
|
default: return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showViewDialog} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>设备详情</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{viewingDevice?.name} - {viewingDevice?.type}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{viewingDevice && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>设备名称</Label>
|
||||||
|
<div className="mt-1 p-2 bg-muted rounded-md text-sm">{viewingDevice.name}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>设备类型</Label>
|
||||||
|
<div className="mt-1 p-2 bg-muted rounded-md text-sm">{viewingDevice.type}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>所属地块</Label>
|
||||||
|
<div className="mt-1 p-2 bg-muted rounded-md text-sm">{viewingDevice.fieldName}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>安装位置</Label>
|
||||||
|
<div className="mt-1 p-2 bg-muted rounded-md text-sm">{viewingDevice.location}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>设备状态</Label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<Badge className={`${getStatusColor(viewingDevice.status)} text-white`}>
|
||||||
|
{getStatusLabel(viewingDevice.status)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>电池电量</Label>
|
||||||
|
<div className="mt-1 p-2 bg-muted rounded-md text-sm">
|
||||||
|
<span className={viewingDevice.battery < 40 ? 'text-red-600 dark:text-red-400' : ''}>
|
||||||
|
{viewingDevice.battery}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Label>最后更新时间</Label>
|
||||||
|
<div className="mt-1 p-2 bg-muted rounded-md text-sm">{viewingDevice.lastUpdate}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>传感器数据</Label>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mt-2">
|
||||||
|
{viewingDevice.sensors.temperature && (
|
||||||
|
<Card className="p-3 bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Thermometer className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">温度</p>
|
||||||
|
<p className="text-sm font-medium">{viewingDevice.sensors.temperature}°C</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{viewingDevice.sensors.humidity && (
|
||||||
|
<Card className="p-3 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Droplets className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">湿度</p>
|
||||||
|
<p className="text-sm font-medium">{viewingDevice.sensors.humidity}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{viewingDevice.sensors.co2 && (
|
||||||
|
<Card className="p-3 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Leaf className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">CO₂</p>
|
||||||
|
<p className="text-sm font-medium">{viewingDevice.sensors.co2} ppm</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{viewingDevice.sensors.light && (
|
||||||
|
<Card className="p-3 bg-yellow-50 dark:bg-yellow-950 border-yellow-200 dark:border-yellow-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Eye className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">光照</p>
|
||||||
|
<p className="text-sm font-medium">{(viewingDevice.sensors.light / 1000).toFixed(1)}k lux</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{viewingDevice.sensors.pm25 && (
|
||||||
|
<Card className="p-3 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wind className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">PM2.5</p>
|
||||||
|
<p className="text-sm font-medium">{viewingDevice.sensors.pm25} μg/m³</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4 border-t">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Eye, Edit, Trash2, Battery } from 'lucide-react';
|
||||||
|
import { SensorDevice } from './environmentMonitoringReducer';
|
||||||
|
|
||||||
|
interface DeviceManagementProps {
|
||||||
|
sensorDevices: SensorDevice[];
|
||||||
|
onViewDevice: (device: SensorDevice) => void;
|
||||||
|
onEditDevice: (device: SensorDevice) => void;
|
||||||
|
onDeleteDevice: (deviceId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceManagement({
|
||||||
|
sensorDevices,
|
||||||
|
onViewDevice,
|
||||||
|
onEditDevice,
|
||||||
|
onDeleteDevice
|
||||||
|
}: DeviceManagementProps) {
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return 'bg-green-500';
|
||||||
|
case 'offline': return 'bg-gray-500';
|
||||||
|
case 'warning': return 'bg-yellow-500';
|
||||||
|
default: return 'bg-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return '在线';
|
||||||
|
case 'offline': return '离线';
|
||||||
|
case 'warning': return '警告';
|
||||||
|
default: return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="text-left p-2">设备名称</th>
|
||||||
|
<th className="text-left p-2">类型</th>
|
||||||
|
<th className="text-left p-2">地块</th>
|
||||||
|
<th className="text-left p-2">位置</th>
|
||||||
|
<th className="text-left p-2">状态</th>
|
||||||
|
<th className="text-left p-2">电量</th>
|
||||||
|
<th className="text-left p-2">最后更新</th>
|
||||||
|
<th className="text-left p-2">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sensorDevices.map((device) => (
|
||||||
|
<tr key={device.id} className="border-b hover:bg-muted/50">
|
||||||
|
<td className="p-2">{device.name}</td>
|
||||||
|
<td className="p-2">{device.type}</td>
|
||||||
|
<td className="p-2">{device.fieldName}</td>
|
||||||
|
<td className="p-2">{device.location}</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<Badge variant="outline" className={`${getStatusColor(device.status)} text-white`}>
|
||||||
|
{getStatusLabel(device.status)}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Battery className={`w-4 h-4 ${device.battery < 40 ? 'text-red-500' : 'text-green-500'}`} />
|
||||||
|
<span className={device.battery < 40 ? 'text-red-600 dark:text-red-400' : ''}>
|
||||||
|
{device.battery}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-sm text-muted-foreground">{device.lastUpdate}</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onViewDevice(device)}
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onEditDevice(device)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDeleteDevice(device.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { CheckCircle2, AlertCircle, Radio, Activity } from 'lucide-react';
|
||||||
|
import { SensorDevice } from './environmentMonitoringReducer';
|
||||||
|
|
||||||
|
interface DeviceOverviewProps {
|
||||||
|
sensorDevices: SensorDevice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceOverview({ sensorDevices }: DeviceOverviewProps) {
|
||||||
|
const onlineDevices = sensorDevices.filter(d => d.status === 'online').length;
|
||||||
|
const warningDevices = sensorDevices.filter(d => d.status === 'warning').length;
|
||||||
|
const offlineDevices = sensorDevices.filter(d => d.status === 'offline').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900 border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">在线设备</p>
|
||||||
|
<p className="mt-2 text-2xl text-green-600 dark:text-green-400">{onlineDevices}</p>
|
||||||
|
</div>
|
||||||
|
<CheckCircle2 className="w-10 h-10 text-green-600 dark:text-green-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-yellow-50 to-yellow-100 dark:from-yellow-950 dark:to-yellow-900 border-yellow-200 dark:border-yellow-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">警告设备</p>
|
||||||
|
<p className="mt-2 text-2xl text-yellow-600 dark:text-yellow-400">{warningDevices}</p>
|
||||||
|
</div>
|
||||||
|
<AlertCircle className="w-10 h-10 text-yellow-600 dark:text-yellow-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 border-gray-200 dark:border-gray-600">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">离线设备</p>
|
||||||
|
<p className="mt-2 text-2xl text-muted-foreground">{offlineDevices}</p>
|
||||||
|
</div>
|
||||||
|
<Radio className="w-10 h-10 text-muted-foreground opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-950 dark:to-blue-900 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">设备总数</p>
|
||||||
|
<p className="mt-2 text-2xl text-blue-600 dark:text-blue-400">{sensorDevices.length}</p>
|
||||||
|
</div>
|
||||||
|
<Activity className="w-10 h-10 text-blue-600 dark:text-blue-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Thermometer,
|
||||||
|
Droplets,
|
||||||
|
Leaf,
|
||||||
|
Eye,
|
||||||
|
Wind,
|
||||||
|
TrendingUp,
|
||||||
|
Battery
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { SensorDevice } from './environmentMonitoringReducer';
|
||||||
|
|
||||||
|
interface RealTimeMonitoringProps {
|
||||||
|
selectedDevice: string;
|
||||||
|
sensorDevices: SensorDevice[];
|
||||||
|
onDeviceChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RealTimeMonitoring({
|
||||||
|
selectedDevice,
|
||||||
|
sensorDevices,
|
||||||
|
onDeviceChange
|
||||||
|
}: RealTimeMonitoringProps) {
|
||||||
|
const currentDevice = sensorDevices.find(d => d.id === selectedDevice) || sensorDevices[0];
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return 'bg-green-500';
|
||||||
|
case 'offline': return 'bg-gray-500';
|
||||||
|
case 'warning': return 'bg-yellow-500';
|
||||||
|
default: return 'bg-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'online': return '在线';
|
||||||
|
case 'offline': return '离线';
|
||||||
|
case 'warning': return '警告';
|
||||||
|
default: return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCO2Level = (co2: number) => {
|
||||||
|
if (co2 < 400) return { label: '正常', color: 'text-green-600 dark:text-green-400' };
|
||||||
|
if (co2 < 600) return { label: '良好', color: 'text-blue-600 dark:text-blue-400' };
|
||||||
|
if (co2 < 1000) return { label: '偏高', color: 'text-yellow-600 dark:text-yellow-400' };
|
||||||
|
return { label: '过高', color: 'text-red-600 dark:text-red-400' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPM25Level = (pm25: number) => {
|
||||||
|
if (pm25 < 35) return { label: '优', color: 'text-green-600 dark:text-green-400' };
|
||||||
|
if (pm25 < 75) return { label: '良', color: 'text-blue-600 dark:text-blue-400' };
|
||||||
|
if (pm25 < 115) return { label: '轻度污染', color: 'text-yellow-600 dark:text-yellow-400' };
|
||||||
|
return { label: '重度污染', color: 'text-red-600 dark:text-red-400' };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-4 mb-4 flex-wrap">
|
||||||
|
<Label className="text-sm">选择设备查看实时数据:</Label>
|
||||||
|
<Select value={selectedDevice} onValueChange={onDeviceChange}>
|
||||||
|
<SelectTrigger className="w-80">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{sensorDevices.map((device) => (
|
||||||
|
<SelectItem key={device.id} value={device.id}>
|
||||||
|
{device.name} - {device.fieldName} - {device.type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Badge variant="outline" className={`${getStatusColor(currentDevice.status)} text-white`}>
|
||||||
|
{getStatusLabel(currentDevice.status)}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="flex items-center gap-1">
|
||||||
|
<Battery className={`w-3 h-3 ${currentDevice.battery < 40 ? 'text-red-500' : 'text-green-500'}`} />
|
||||||
|
电量: {currentDevice.battery}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-red-50 to-red-100 dark:from-red-950 dark:to-red-900 border-red-200 dark:border-red-800">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Thermometer className="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||||
|
<Badge variant="outline">实时</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">空气温度</p>
|
||||||
|
<p className="mt-2 text-2xl text-red-600 dark:text-red-400">{currentDevice.sensors.temperature}°C</p>
|
||||||
|
<p className="text-xs text-green-600 dark:text-green-400 mt-1">↑ 0.5°C</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-950 dark:to-blue-900 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Droplets className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
<Badge variant="outline">实时</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">空气湿度</p>
|
||||||
|
<p className="mt-2 text-2xl text-blue-600 dark:text-blue-400">{currentDevice.sensors.humidity}%</p>
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 mt-1">↓ 3%</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900 border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Leaf className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
<Badge variant="outline">实时</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">CO₂浓度</p>
|
||||||
|
<p className="mt-2 text-2xl text-green-600 dark:text-green-400">{currentDevice.sensors.co2}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">ppm</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-yellow-50 to-yellow-100 dark:from-yellow-950 dark:to-yellow-900 border-yellow-200 dark:border-yellow-800">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Eye className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
|
||||||
|
<Badge variant="outline">实时</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">光照强度</p>
|
||||||
|
<p className="mt-2 text-2xl text-yellow-600 dark:text-yellow-400">{(currentDevice.sensors.light! / 1000).toFixed(0)}k</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">lux</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-950 dark:to-purple-900 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Wind className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
<Badge variant="outline">实时</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">PM2.5</p>
|
||||||
|
<p className="mt-2 text-2xl text-purple-600 dark:text-purple-400">{currentDevice.sensors.pm25}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">μg/m³</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-green-50 to-white dark:from-green-950 dark:to-card border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">CO₂水平评估</p>
|
||||||
|
<p className={`mt-2 ${getCO2Level(currentDevice.sensors.co2!).color}`}>
|
||||||
|
{getCO2Level(currentDevice.sensors.co2!).label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TrendingUp className="w-8 h-8 text-green-600 dark:text-green-400 opacity-30" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-gradient-to-br from-purple-50 to-white dark:from-purple-950 dark:to-card border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">空气质量评估</p>
|
||||||
|
<p className={`mt-2 ${getPM25Level(currentDevice.sensors.pm25!).color}`}>
|
||||||
|
{getPM25Level(currentDevice.sensors.pm25!).label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Wind className="w-8 h-8 text-purple-600 dark:text-purple-400 opacity-30" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
export interface SensorDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
location: string;
|
||||||
|
fieldId: string;
|
||||||
|
fieldName: string;
|
||||||
|
status: 'online' | 'offline' | 'warning';
|
||||||
|
battery: number;
|
||||||
|
lastUpdate: string;
|
||||||
|
sensors: {
|
||||||
|
temperature?: number;
|
||||||
|
humidity?: number;
|
||||||
|
co2?: number;
|
||||||
|
light?: number;
|
||||||
|
pm25?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IoTDevice {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
manufacturer: string;
|
||||||
|
model: string;
|
||||||
|
location: string;
|
||||||
|
fieldId: string;
|
||||||
|
fieldName: string;
|
||||||
|
protocol: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
mqttTopic?: string;
|
||||||
|
status: string;
|
||||||
|
bindingStatus: '未绑定' | '已绑定';
|
||||||
|
bindingSystem?: string;
|
||||||
|
lastReportTime: string;
|
||||||
|
dataFrequency: string;
|
||||||
|
batteryLevel?: number;
|
||||||
|
signalStrength?: number;
|
||||||
|
sensors: {
|
||||||
|
name: string;
|
||||||
|
unit: string;
|
||||||
|
currentValue: number;
|
||||||
|
normalRange: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvironmentMonitoringState {
|
||||||
|
selectedDevice: string;
|
||||||
|
showDeviceDialog: boolean;
|
||||||
|
showViewDialog: boolean;
|
||||||
|
editingDevice: SensorDevice | null;
|
||||||
|
viewingDevice: SensorDevice | null;
|
||||||
|
timeRange: '1h' | '6h' | '24h' | '7d';
|
||||||
|
availableIoTDevices: IoTDevice[];
|
||||||
|
sensorDevices: SensorDevice[];
|
||||||
|
deviceForm: {
|
||||||
|
iotDeviceId: string;
|
||||||
|
location: string;
|
||||||
|
fieldId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnvironmentMonitoringAction =
|
||||||
|
| { type: 'SET_SELECTED_DEVICE'; payload: string }
|
||||||
|
| { type: 'SET_SHOW_DEVICE_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_SHOW_VIEW_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_EDITING_DEVICE'; payload: SensorDevice | null }
|
||||||
|
| { type: 'SET_VIEWING_DEVICE'; payload: SensorDevice | null }
|
||||||
|
| { type: 'SET_TIME_RANGE'; payload: '1h' | '6h' | '24h' | '7d' }
|
||||||
|
| { type: 'SET_AVAILABLE_IOT_DEVICES'; payload: IoTDevice[] }
|
||||||
|
| { type: 'SET_SENSOR_DEVICES'; payload: SensorDevice[] }
|
||||||
|
| { type: 'SET_DEVICE_FORM'; payload: Partial<EnvironmentMonitoringState['deviceForm']> }
|
||||||
|
| { type: 'ADD_SENSOR_DEVICE'; payload: SensorDevice }
|
||||||
|
| { type: 'UPDATE_SENSOR_DEVICE'; payload: { id: string; updates: Partial<SensorDevice> } }
|
||||||
|
| { type: 'DELETE_SENSOR_DEVICE'; payload: string }
|
||||||
|
| { type: 'LOAD_IOT_DEVICES' }
|
||||||
|
| { type: 'RESET_DEVICE_FORM' };
|
||||||
|
|
||||||
|
const availableFields = [
|
||||||
|
{ id: 'field-1', name: '东区1号地' },
|
||||||
|
{ id: 'field-2', name: '西区2号地' },
|
||||||
|
{ id: 'field-3', name: '南区3号地' },
|
||||||
|
{ id: 'field-4', name: '北区4号地' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialSensorDevices: SensorDevice[] = [
|
||||||
|
{
|
||||||
|
id: 'device-1',
|
||||||
|
name: 'ENV-001',
|
||||||
|
type: '综合环境监测站',
|
||||||
|
location: '东区1号地中心',
|
||||||
|
fieldId: 'field-1',
|
||||||
|
fieldName: '东区1号地',
|
||||||
|
status: 'online',
|
||||||
|
battery: 85,
|
||||||
|
lastUpdate: '2024-10-18 15:30',
|
||||||
|
sensors: {
|
||||||
|
temperature: 24.5,
|
||||||
|
humidity: 62,
|
||||||
|
co2: 420,
|
||||||
|
light: 45000,
|
||||||
|
pm25: 35,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device-2',
|
||||||
|
name: 'ENV-002',
|
||||||
|
type: '气象传感器',
|
||||||
|
location: '西区2号地北侧',
|
||||||
|
fieldId: 'field-2',
|
||||||
|
fieldName: '西区2号地',
|
||||||
|
status: 'online',
|
||||||
|
battery: 92,
|
||||||
|
lastUpdate: '2024-10-18 15:28',
|
||||||
|
sensors: {
|
||||||
|
temperature: 23.8,
|
||||||
|
humidity: 68,
|
||||||
|
co2: 410,
|
||||||
|
light: 42000,
|
||||||
|
pm25: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device-3',
|
||||||
|
name: 'ENV-003',
|
||||||
|
type: 'CO2监测器',
|
||||||
|
location: '南区3号地温室',
|
||||||
|
fieldId: 'field-3',
|
||||||
|
fieldName: '南区3号地',
|
||||||
|
status: 'warning',
|
||||||
|
battery: 35,
|
||||||
|
lastUpdate: '2024-10-18 15:25',
|
||||||
|
sensors: {
|
||||||
|
temperature: 26.2,
|
||||||
|
humidity: 75,
|
||||||
|
co2: 550,
|
||||||
|
light: 38000,
|
||||||
|
pm25: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const initialState: EnvironmentMonitoringState = {
|
||||||
|
selectedDevice: 'device-1',
|
||||||
|
showDeviceDialog: false,
|
||||||
|
showViewDialog: false,
|
||||||
|
editingDevice: null,
|
||||||
|
viewingDevice: null,
|
||||||
|
timeRange: '24h',
|
||||||
|
availableIoTDevices: [],
|
||||||
|
sensorDevices: initialSensorDevices,
|
||||||
|
deviceForm: {
|
||||||
|
iotDeviceId: '',
|
||||||
|
location: '',
|
||||||
|
fieldId: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function environmentMonitoringReducer(
|
||||||
|
state: EnvironmentMonitoringState,
|
||||||
|
action: EnvironmentMonitoringAction
|
||||||
|
): EnvironmentMonitoringState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_SELECTED_DEVICE':
|
||||||
|
return { ...state, selectedDevice: action.payload };
|
||||||
|
|
||||||
|
case 'SET_SHOW_DEVICE_DIALOG':
|
||||||
|
return { ...state, showDeviceDialog: action.payload };
|
||||||
|
|
||||||
|
case 'SET_SHOW_VIEW_DIALOG':
|
||||||
|
return { ...state, showViewDialog: action.payload };
|
||||||
|
|
||||||
|
case 'SET_EDITING_DEVICE':
|
||||||
|
return { ...state, editingDevice: action.payload };
|
||||||
|
|
||||||
|
case 'SET_VIEWING_DEVICE':
|
||||||
|
return { ...state, viewingDevice: action.payload };
|
||||||
|
|
||||||
|
case 'SET_TIME_RANGE':
|
||||||
|
return { ...state, timeRange: action.payload };
|
||||||
|
|
||||||
|
case 'SET_AVAILABLE_IOT_DEVICES':
|
||||||
|
return { ...state, availableIoTDevices: action.payload };
|
||||||
|
|
||||||
|
case 'SET_SENSOR_DEVICES':
|
||||||
|
return { ...state, sensorDevices: action.payload };
|
||||||
|
|
||||||
|
case 'SET_DEVICE_FORM':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceForm: { ...state.deviceForm, ...action.payload }
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'ADD_SENSOR_DEVICE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sensorDevices: [...state.sensorDevices, action.payload]
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_SENSOR_DEVICE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sensorDevices: state.sensorDevices.map(device =>
|
||||||
|
device.id === action.payload.id
|
||||||
|
? { ...device, ...action.payload.updates }
|
||||||
|
: device
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'DELETE_SENSOR_DEVICE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sensorDevices: state.sensorDevices.filter(device => device.id !== action.payload)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'LOAD_IOT_DEVICES':
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem('smart_agriculture_ai_iot_devices');
|
||||||
|
if (data) {
|
||||||
|
const devices: IoTDevice[] = JSON.parse(data);
|
||||||
|
const environmentDevices = devices.filter(
|
||||||
|
d => d.type === '环境监测站' && d.bindingStatus === '未绑定'
|
||||||
|
);
|
||||||
|
return { ...state, availableIoTDevices: environmentDevices };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load IoT devices:', error);
|
||||||
|
}
|
||||||
|
return { ...state, availableIoTDevices: [] };
|
||||||
|
|
||||||
|
case 'RESET_DEVICE_FORM':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deviceForm: {
|
||||||
|
iotDeviceId: '',
|
||||||
|
location: '',
|
||||||
|
fieldId: '',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,229 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
import { useReducer, useEffect } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import {
|
||||||
|
environmentMonitoringReducer,
|
||||||
|
initialState,
|
||||||
|
EnvironmentMonitoringAction,
|
||||||
|
SensorDevice,
|
||||||
|
IoTDevice
|
||||||
|
} from './components/environmentMonitoringReducer';
|
||||||
|
import { DeviceOverview } from './components/DeviceOverview';
|
||||||
|
import { RealTimeMonitoring } from './components/RealTimeMonitoring';
|
||||||
|
import { DeviceManagement } from './components/DeviceManagement';
|
||||||
|
import { DataTrends } from './components/DataTrends';
|
||||||
|
import { DeviceDialog, DeviceViewDialog } from './components/DeviceDialog';
|
||||||
|
|
||||||
|
const availableFields = [
|
||||||
|
{ id: 'field-1', name: '东区1号地' },
|
||||||
|
{ id: 'field-2', name: '西区2号地' },
|
||||||
|
{ id: 'field-3', name: '南区3号地' },
|
||||||
|
{ id: 'field-4', name: '北区4号地' },
|
||||||
|
];
|
||||||
|
|
||||||
export default function EnvironmentPage() {
|
export default function EnvironmentPage() {
|
||||||
|
const [state, dispatch] = useReducer(environmentMonitoringReducer, initialState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'LOAD_IOT_DEVICES' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeviceChange = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_DEVICE', payload: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeRangeChange = (value: '1h' | '6h' | '24h' | '7d') => {
|
||||||
|
dispatch({ type: 'SET_TIME_RANGE', payload: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddDevice = () => {
|
||||||
|
dispatch({ type: 'SET_EDITING_DEVICE', payload: null });
|
||||||
|
dispatch({ type: 'RESET_DEVICE_FORM' });
|
||||||
|
dispatch({ type: 'SET_SHOW_DEVICE_DIALOG', payload: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditDevice = (device: SensorDevice) => {
|
||||||
|
dispatch({ type: 'SET_EDITING_DEVICE', payload: device });
|
||||||
|
dispatch({ type: 'SET_DEVICE_FORM', payload: {
|
||||||
|
iotDeviceId: '',
|
||||||
|
location: device.location,
|
||||||
|
fieldId: device.fieldId,
|
||||||
|
}});
|
||||||
|
dispatch({ type: 'SET_SHOW_DEVICE_DIALOG', payload: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDevice = (device: SensorDevice) => {
|
||||||
|
dispatch({ type: 'SET_VIEWING_DEVICE', payload: device });
|
||||||
|
dispatch({ type: 'SET_SHOW_VIEW_DIALOG', payload: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteDevice = (deviceId: string) => {
|
||||||
|
if (confirm('确定要删除该设备吗?')) {
|
||||||
|
dispatch({ type: 'DELETE_SENSOR_DEVICE', payload: deviceId });
|
||||||
|
toast.success('设备已删除');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeviceFormChange = (form: Partial<typeof state.deviceForm>) => {
|
||||||
|
dispatch({ type: 'SET_DEVICE_FORM', payload: form });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveDevice = () => {
|
||||||
|
if (!state.deviceForm.fieldId) {
|
||||||
|
toast.error('请选择地块');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = availableFields.find(f => f.id === state.deviceForm.fieldId);
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
toast.error('无效的地块选择');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.editingDevice) {
|
||||||
|
// 编辑设备
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_SENSOR_DEVICE',
|
||||||
|
payload: {
|
||||||
|
id: state.editingDevice.id,
|
||||||
|
updates: {
|
||||||
|
location: state.deviceForm.location,
|
||||||
|
fieldId: state.deviceForm.fieldId,
|
||||||
|
fieldName: field.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
toast.success('设备信息已更新');
|
||||||
|
} else {
|
||||||
|
// 添加新设备
|
||||||
|
if (!state.deviceForm.iotDeviceId) {
|
||||||
|
toast.error('请选择设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iotDevice = state.availableIoTDevices.find(d => d.id === state.deviceForm.iotDeviceId);
|
||||||
|
|
||||||
|
if (!iotDevice) {
|
||||||
|
toast.error('无效的设备选择');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDevice: SensorDevice = {
|
||||||
|
id: `device-${Date.now()}`,
|
||||||
|
name: iotDevice.code,
|
||||||
|
type: iotDevice.type,
|
||||||
|
location: state.deviceForm.location,
|
||||||
|
fieldId: state.deviceForm.fieldId,
|
||||||
|
fieldName: field.name,
|
||||||
|
status: 'online',
|
||||||
|
battery: iotDevice.batteryLevel || 100,
|
||||||
|
lastUpdate: new Date().toLocaleString('zh-CN'),
|
||||||
|
sensors: {
|
||||||
|
temperature: 22,
|
||||||
|
humidity: 60,
|
||||||
|
co2: 400,
|
||||||
|
light: 40000,
|
||||||
|
pm25: 30,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({ type: 'ADD_SENSOR_DEVICE', payload: newDevice });
|
||||||
|
|
||||||
|
// 更新AI数据中心的设备绑定状态
|
||||||
|
try {
|
||||||
|
const allIoTDevices = localStorage.getItem('smart_agriculture_ai_iot_devices');
|
||||||
|
if (allIoTDevices) {
|
||||||
|
const devices: IoTDevice[] = JSON.parse(allIoTDevices);
|
||||||
|
const updatedDevices = devices.map(d =>
|
||||||
|
d.id === state.deviceForm.iotDeviceId
|
||||||
|
? { ...d, bindingStatus: '已绑定' as const, bindingSystem: '地块环境监测-环境监测' }
|
||||||
|
: d
|
||||||
|
);
|
||||||
|
localStorage.setItem('smart_agriculture_ai_iot_devices', JSON.stringify(updatedDevices));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update IoT device binding status:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('设备添加成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_SHOW_DEVICE_DIALOG', payload: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelDialog = () => {
|
||||||
|
dispatch({ type: 'SET_SHOW_DEVICE_DIALOG', payload: false });
|
||||||
|
dispatch({ type: 'SET_SHOW_VIEW_DIALOG', payload: false });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold">环境监测</h2>
|
<div>
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
<h2 className="text-green-800 dark:text-green-200">环境监测</h2>
|
||||||
<p className="text-sm">
|
<p className="text-muted-foreground">
|
||||||
<strong>页面路径:</strong> /land-information/monitoring/environment
|
物联网传感器管理、环境数据采集与分析
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className="flex gap-2">
|
||||||
|
<Button className="bg-green-600 hover:bg-green-700" onClick={handleAddDevice}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加设备
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeviceOverview sensorDevices={state.sensorDevices} />
|
||||||
|
|
||||||
|
<RealTimeMonitoring
|
||||||
|
selectedDevice={state.selectedDevice}
|
||||||
|
sensorDevices={state.sensorDevices}
|
||||||
|
onDeviceChange={handleDeviceChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs defaultValue="devices" className="w-full">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="devices">设备管理</TabsTrigger>
|
||||||
|
<TabsTrigger value="trends">数据趋势</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="devices">
|
||||||
|
<DeviceManagement
|
||||||
|
sensorDevices={state.sensorDevices}
|
||||||
|
onViewDevice={handleViewDevice}
|
||||||
|
onEditDevice={handleEditDevice}
|
||||||
|
onDeleteDevice={handleDeleteDevice}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="trends">
|
||||||
|
<DataTrends
|
||||||
|
timeRange={state.timeRange}
|
||||||
|
onTimeRangeChange={handleTimeRangeChange}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<DeviceDialog
|
||||||
|
showDeviceDialog={state.showDeviceDialog}
|
||||||
|
editingDevice={state.editingDevice}
|
||||||
|
deviceForm={state.deviceForm}
|
||||||
|
availableIoTDevices={state.availableIoTDevices}
|
||||||
|
onDeviceFormChange={handleDeviceFormChange}
|
||||||
|
onSaveDevice={handleSaveDevice}
|
||||||
|
onCancel={handleCancelDialog}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeviceViewDialog
|
||||||
|
showViewDialog={state.showViewDialog}
|
||||||
|
viewingDevice={state.viewingDevice}
|
||||||
|
onClose={handleCancelDialog}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { WeatherDataState, WeatherDataAction } from './weatherDataReducer';
|
||||||
|
|
||||||
|
interface ControlPanelProps {
|
||||||
|
state: WeatherDataState;
|
||||||
|
dispatch: React.Dispatch<WeatherDataAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ControlPanel({ state, dispatch }: ControlPanelProps) {
|
||||||
|
const handleFieldChange = (fieldId: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { selectedField: fieldId } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeRangeChange = (timeRange: '24h' | '7d' | '30d') => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { timeRange } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">选择地块</label>
|
||||||
|
<Select value={state.filters.selectedField} onValueChange={handleFieldChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="field-1">东区1号地</SelectItem>
|
||||||
|
<SelectItem value="field-2">西区2号地</SelectItem>
|
||||||
|
<SelectItem value="field-3">南区3号地</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground mb-2 block">时间范围</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant={state.filters.timeRange === '24h' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleTimeRangeChange('24h')}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
24小时
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={state.filters.timeRange === '7d' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleTimeRangeChange('7d')}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
7天
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={state.filters.timeRange === '30d' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleTimeRangeChange('30d')}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
30天
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end">
|
||||||
|
<div className="text-xs text-muted-foreground text-right">
|
||||||
|
<div>最后更新:</div>
|
||||||
|
<div className="font-medium">{state.lastUpdateTime || '未更新'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { WeatherData } from './weatherDataReducer';
|
||||||
|
import {
|
||||||
|
Thermometer,
|
||||||
|
Droplets,
|
||||||
|
Wind,
|
||||||
|
CloudRain,
|
||||||
|
Sun,
|
||||||
|
Activity,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface RealtimeWeatherCardsProps {
|
||||||
|
weatherData: WeatherData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RealtimeWeatherCards({ weatherData }: RealtimeWeatherCardsProps) {
|
||||||
|
// 获取最新的数据(最后一个数据点)
|
||||||
|
const latestData = weatherData[weatherData.length - 1];
|
||||||
|
|
||||||
|
// 如果没有数据,显示占位符
|
||||||
|
if (!latestData) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<Card key={i} className="p-4 animate-pulse">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-3 bg-muted rounded w-16"></div>
|
||||||
|
<div className="h-6 bg-muted rounded w-12"></div>
|
||||||
|
</div>
|
||||||
|
<div className="w-10 h-10 bg-muted rounded"></div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算变化趋势(与上一个数据点比较)
|
||||||
|
const previousData = weatherData[weatherData.length - 2];
|
||||||
|
const getTrend = (current: number, previous?: number) => {
|
||||||
|
if (!previous) return null;
|
||||||
|
const diff = current - previous;
|
||||||
|
if (Math.abs(diff) < 0.1) return null;
|
||||||
|
return diff > 0 ? 'up' : 'down';
|
||||||
|
};
|
||||||
|
|
||||||
|
const tempTrend = getTrend(latestData.temperature, previousData?.temperature);
|
||||||
|
const humidityTrend = getTrend(latestData.humidity, previousData?.humidity);
|
||||||
|
const windTrend = getTrend(latestData.windSpeed, previousData?.windSpeed);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
{/* 温度 */}
|
||||||
|
<Card className="p-4 bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">温度</p>
|
||||||
|
<p className="mt-2 text-2xl text-red-600 dark:text-red-400 font-semibold">
|
||||||
|
{latestData.temperature}°C
|
||||||
|
</p>
|
||||||
|
{tempTrend && (
|
||||||
|
<p className={`text-xs mt-1 ${tempTrend === 'up' ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{tempTrend === 'up' ? '↑' : '↓'} {Math.abs(latestData.temperature - (previousData?.temperature || 0)).toFixed(1)}°C
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Thermometer className="w-10 h-10 text-red-600 dark:text-red-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 湿度 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">湿度</p>
|
||||||
|
<p className="mt-2 text-2xl text-blue-600 dark:text-blue-400 font-semibold">
|
||||||
|
{latestData.humidity}%
|
||||||
|
</p>
|
||||||
|
{humidityTrend && (
|
||||||
|
<p className={`text-xs mt-1 ${humidityTrend === 'up' ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{humidityTrend === 'up' ? '↑' : '↓'} {Math.abs(latestData.humidity - (previousData?.humidity || 0)).toFixed(0)}%
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Droplets className="w-10 h-10 text-blue-600 dark:text-blue-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 风速 */}
|
||||||
|
<Card className="p-4 bg-cyan-50 dark:bg-cyan-950 border-cyan-200 dark:border-cyan-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">风速</p>
|
||||||
|
<p className="mt-2 text-2xl text-cyan-600 dark:text-cyan-400 font-semibold">
|
||||||
|
{latestData.windSpeed} m/s
|
||||||
|
</p>
|
||||||
|
{windTrend && (
|
||||||
|
<p className={`text-xs mt-1 ${windTrend === 'up' ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{windTrend === 'up' ? '↑' : '↓'} {Math.abs(latestData.windSpeed - (previousData?.windSpeed || 0)).toFixed(1)} m/s
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Wind className="w-10 h-10 text-cyan-600 dark:text-cyan-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 降雨量 */}
|
||||||
|
<Card className="p-4 bg-indigo-50 dark:bg-indigo-950 border-indigo-200 dark:border-indigo-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">降雨量</p>
|
||||||
|
<p className="mt-2 text-2xl text-indigo-600 dark:text-indigo-400 font-semibold">
|
||||||
|
{latestData.rainfall} mm
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{weatherData.length > 0 ? '今日累计' : '暂无数据'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CloudRain className="w-10 h-10 text-indigo-600 dark:text-indigo-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 光照强度 */}
|
||||||
|
<Card className="p-4 bg-yellow-50 dark:bg-yellow-950 border-yellow-200 dark:border-yellow-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">光照强度</p>
|
||||||
|
<p className="mt-2 text-2xl text-yellow-600 dark:text-yellow-400 font-semibold">
|
||||||
|
{latestData.sunlight}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">W/m²</p>
|
||||||
|
</div>
|
||||||
|
<Sun className="w-10 h-10 text-yellow-600 dark:text-yellow-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 气压 */}
|
||||||
|
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">气压</p>
|
||||||
|
<p className="mt-2 text-2xl text-purple-600 dark:text-purple-400 font-semibold">
|
||||||
|
{latestData.pressure}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">hPa</p>
|
||||||
|
</div>
|
||||||
|
<Activity className="w-10 h-10 text-purple-600 dark:text-purple-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Zap } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function UsageGuide() {
|
||||||
|
return (
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Zap className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="mb-2 font-medium">气象监测功能说明:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>API接口</strong>: 支持对接多种气象API(如中国气象局、和风天气等)</li>
|
||||||
|
<li>• <strong>实时监控</strong>: 自动获取并更新气象站数据(每15分钟自动刷新)</li>
|
||||||
|
<li>• <strong>数据可视化</strong>: 多种图表展示气象数据变化趋势</li>
|
||||||
|
<li>• <strong>天气预报</strong>: 对接天气预报API,提供未来7天预报</li>
|
||||||
|
<li>• <strong>灾害预警</strong>: 智能识别低温冻害(≤3°C)、大风(≥10m/s)、暴雨(≥50mm)、高温(≥35°C)</li>
|
||||||
|
<li>• <strong>历史数据</strong>: 完整的历史气象数据记录和查询功能</li>
|
||||||
|
<li>• <strong>统计分析</strong>: 自动计算气象统计指标,提供农业建议</li>
|
||||||
|
<li>• <strong>数据导出</strong>: 支持导出CSV格式的气象数据</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
import { WeatherDataState, WeatherDataAction, getAlertColor } from './weatherDataReducer';
|
||||||
|
|
||||||
|
interface WeatherAlertsProps {
|
||||||
|
state: WeatherDataState;
|
||||||
|
dispatch: React.Dispatch<WeatherDataAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeatherAlerts({ state, dispatch }: WeatherAlertsProps) {
|
||||||
|
const unacknowledgedAlerts = state.weatherAlerts.filter(
|
||||||
|
alert => !state.acknowledgedAlerts.includes(alert.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAcknowledgeAlert = (alertId: string) => {
|
||||||
|
dispatch({ type: 'ACKNOWLEDGE_ALERT', payload: alertId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAcknowledgeAll = () => {
|
||||||
|
dispatch({ type: 'ACKNOWLEDGE_ALL_ALERTS' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (unacknowledgedAlerts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-orange-800 dark:text-orange-200">
|
||||||
|
天气预警 ({unacknowledgedAlerts.length})
|
||||||
|
</h3>
|
||||||
|
{unacknowledgedAlerts.length > 1 && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleAcknowledgeAll}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
全部知晓
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{unacknowledgedAlerts.map((alert) => (
|
||||||
|
<Card
|
||||||
|
key={alert.id}
|
||||||
|
className="p-4 border-l-4 border-l-orange-500 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<AlertTriangle className="w-6 h-6 text-orange-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<Badge className={`${getAlertColor(alert.level)} text-white font-light`}>
|
||||||
|
{alert.level}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="font-light">{alert.type}</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">{alert.time}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm mb-2 text-orange-800 dark:text-orange-200">{alert.message}</p>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>影响地块:</span>
|
||||||
|
{alert.affectedFields.map((field, index) => (
|
||||||
|
<Badge key={index} variant="outline" className="font-light">
|
||||||
|
{field}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={() => handleAcknowledgeAlert(alert.id)}
|
||||||
|
>
|
||||||
|
知晓
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { WeatherStatistics, WeatherData } from './weatherDataReducer';
|
||||||
|
|
||||||
|
interface WeatherAnalysisProps {
|
||||||
|
statistics: WeatherStatistics | null;
|
||||||
|
weatherData: WeatherData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeatherAnalysis({ statistics, weatherData }: WeatherAnalysisProps) {
|
||||||
|
if (!statistics) {
|
||||||
|
return (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
<p>暂无统计数据</p>
|
||||||
|
<p className="text-sm mt-1">请等待数据加载完成</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTemperatureLevel = (temp: number) => {
|
||||||
|
if (temp < 0) return { label: '严寒', color: 'text-blue-700' };
|
||||||
|
if (temp < 10) return { label: '寒冷', color: 'text-blue-600' };
|
||||||
|
if (temp < 20) return { label: '凉爽', color: 'text-green-600' };
|
||||||
|
if (temp < 30) return { label: '温暖', color: 'text-orange-500' };
|
||||||
|
return { label: '炎热', color: 'text-red-600' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRainfallLevel = (rainfall: number) => {
|
||||||
|
if (rainfall === 0) return { label: '无雨', color: 'text-gray-500' };
|
||||||
|
if (rainfall < 10) return { label: '小雨', color: 'text-blue-400' };
|
||||||
|
if (rainfall < 25) return { label: '中雨', color: 'text-blue-500' };
|
||||||
|
if (rainfall < 50) return { label: '大雨', color: 'text-blue-600' };
|
||||||
|
return { label: '暴雨', color: 'text-blue-700' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWindLevel = (speed: number) => {
|
||||||
|
if (speed < 1.6) return { label: '软风', color: 'text-green-500' };
|
||||||
|
if (speed < 3.4) return { label: '轻风', color: 'text-green-600' };
|
||||||
|
if (speed < 5.5) return { label: '微风', color: 'text-yellow-600' };
|
||||||
|
if (speed < 8.0) return { label: '和风', color: 'text-orange-500' };
|
||||||
|
if (speed < 10.8) return { label: '清风', color: 'text-orange-600' };
|
||||||
|
return { label: '强风', color: 'text-red-600' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const avgTemp = statistics.temperature.avg;
|
||||||
|
const avgHumidity = statistics.other.avgHumidity;
|
||||||
|
const totalRainfall = statistics.precipitation.total;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* 温度统计 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">温度统计</h3>
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">最高温度:</span>
|
||||||
|
<span className="text-red-600 font-medium">{statistics.temperature.max}°C</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">最低温度:</span>
|
||||||
|
<span className="text-blue-600 font-medium">{statistics.temperature.min}°C</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">平均温度:</span>
|
||||||
|
<span className="font-medium">{statistics.temperature.avg}°C</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">温差:</span>
|
||||||
|
<span className="font-medium">{statistics.temperature.range}°C</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t">
|
||||||
|
<span className="text-muted-foreground">温度等级:</span>
|
||||||
|
<Badge variant="outline" className={getTemperatureLevel(avgTemp).color}>
|
||||||
|
{getTemperatureLevel(avgTemp).label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 降水统计 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">降水统计</h3>
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">累计降雨:</span>
|
||||||
|
<span className="text-blue-600 font-medium">{statistics.precipitation.total} mm</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">最大降雨:</span>
|
||||||
|
<span className="font-medium">{statistics.precipitation.maxHourly} mm/3h</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">降雨时段:</span>
|
||||||
|
<span className="font-medium">{statistics.precipitation.timeRange}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">降雨天数:</span>
|
||||||
|
<span className="font-medium">{weatherData.filter(d => d.rainfall > 0).length} 天</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t">
|
||||||
|
<span className="text-muted-foreground">降雨强度:</span>
|
||||||
|
<Badge variant="outline" className={getRainfallLevel(totalRainfall).color}>
|
||||||
|
{getRainfallLevel(totalRainfall).label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 风力统计 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">风力统计</h3>
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">最大风速:</span>
|
||||||
|
<span className="text-cyan-600 font-medium">{statistics.wind.maxSpeed} m/s</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">平均风速:</span>
|
||||||
|
<span className="font-medium">{statistics.wind.avgSpeed} m/s</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">风力等级:</span>
|
||||||
|
<span className="font-medium">{statistics.wind.level}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">主导风向:</span>
|
||||||
|
<span className="font-medium">{statistics.wind.direction}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t">
|
||||||
|
<span className="text-muted-foreground">风力状态:</span>
|
||||||
|
<Badge variant="outline" className={getWindLevel(statistics.wind.avgSpeed).color}>
|
||||||
|
{getWindLevel(statistics.wind.avgSpeed).label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 综合气象条件评价 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">气象条件综合评价</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<h4 className="text-green-900 dark:text-green-100 mb-2 font-medium">有利条件</h4>
|
||||||
|
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
|
||||||
|
<li>• 温度适宜,{avgTemp >= 15 && avgTemp <= 25 ? '利于作物生长' : '温度条件一般'}</li>
|
||||||
|
<li>• {statistics.other.totalSunlight > 1000 ? '光照充足,光合作用强' : '光照条件适中'}</li>
|
||||||
|
<li>• {totalRainfall > 0 && totalRainfall < 50 ? '适量降雨,补充土壤水分' : totalRainfall === 0 ? '天气干燥,需要灌溉' : '降雨较多,注意排水'}</li>
|
||||||
|
<li>• {avgHumidity >= 40 && avgHumidity <= 70 ? '湿度适中,有利于作物发育' : '湿度条件需要关注'}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-orange-50 dark:bg-orange-950 border border-orange-200 dark:border-orange-800 rounded-lg">
|
||||||
|
<h4 className="text-orange-900 dark:text-orange-100 mb-2 font-medium">注意事项</h4>
|
||||||
|
<ul className="text-sm text-orange-800 dark:text-orange-200 space-y-1">
|
||||||
|
<li>• {statistics.temperature.range > 15 ? '昼夜温差较大,注意保温措施' : '温差适中,作物适应性好'}</li>
|
||||||
|
<li>• {statistics.wind.maxSpeed > 5 ? '风力较大,加固农业设施' : '风力条件良好'}</li>
|
||||||
|
<li>• {statistics.other.avgPressure < 1000 ? '气压偏低,关注天气变化' : '气压稳定,天气状况良好'}</li>
|
||||||
|
<li>• 密切关注未来天气变化趋势</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 农业生产建议 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">农业生产建议</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
|
<h4 className="text-blue-900 dark:text-blue-100 mb-2 font-medium">播种建议</h4>
|
||||||
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
{avgTemp >= 15 && avgTemp <= 25 && totalRainfall < 30 ?
|
||||||
|
'当前气象条件适宜播种,建议抓住有利时机进行播种作业' :
|
||||||
|
'气象条件一般,建议等待更适宜的天气条件'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<h4 className="text-green-900 dark:text-green-100 mb-2 font-medium">施肥建议</h4>
|
||||||
|
<p className="text-sm text-green-800 dark:text-green-200">
|
||||||
|
{avgHumidity >= 50 && avgHumidity <= 70 ?
|
||||||
|
'湿度条件适宜,有利于肥料吸收和利用' :
|
||||||
|
'湿度条件需要调节,建议结合灌溉进行施肥'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800 rounded-lg">
|
||||||
|
<h4 className="text-purple-900 dark:text-purple-100 mb-2 font-medium">病虫害防治</h4>
|
||||||
|
<p className="text-sm text-purple-800 dark:text-purple-200">
|
||||||
|
{statistics.wind.avgSpeed < 3 ?
|
||||||
|
'风速较小,有利于喷药作业,建议进行病虫害防治' :
|
||||||
|
'风力较大,不适合喷药作业,建议等待风力减小'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 气象风险等级 */}
|
||||||
|
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-lg">
|
||||||
|
<h4 className="text-gray-900 dark:text-gray-100 mb-2 font-medium">气象风险等级</h4>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">综合风险:</span>
|
||||||
|
<Badge
|
||||||
|
variant={statistics.wind.maxSpeed > 8 || statistics.precipitation.total > 50 ? 'destructive' :
|
||||||
|
statistics.wind.maxSpeed > 5 || statistics.precipitation.total > 25 ? 'default' : 'secondary'}
|
||||||
|
className="font-light"
|
||||||
|
>
|
||||||
|
{statistics.wind.maxSpeed > 8 || statistics.precipitation.total > 50 ? '高风险' :
|
||||||
|
statistics.wind.maxSpeed > 5 || statistics.precipitation.total > 25 ? '中等风险' : '低风险'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
建议密切关注天气变化,做好防范措施
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Legend,
|
||||||
|
} from 'recharts';
|
||||||
|
import { WeatherData } from './weatherDataReducer';
|
||||||
|
|
||||||
|
interface WeatherChartsProps {
|
||||||
|
weatherData: WeatherData[];
|
||||||
|
timeRange: '24h' | '7d' | '30d';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeatherCharts({ weatherData, timeRange }: WeatherChartsProps) {
|
||||||
|
// 根据时间范围调整图表高度
|
||||||
|
const chartHeight = timeRange === '24h' ? 280 : 220;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 温度变化曲线 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">温度变化曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||||
|
<LineChart data={weatherData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
label={{
|
||||||
|
value: timeRange === '24h' ? '时间' : '日期',
|
||||||
|
position: 'insideBottom',
|
||||||
|
offset: -5,
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
label={{
|
||||||
|
value: '温度 (°C)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value}°C`, '温度']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="temperature"
|
||||||
|
stroke="#dc2626"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#dc2626', r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
{/* 湿度变化曲线 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">湿度变化曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={weatherData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
label={{
|
||||||
|
value: '湿度 (%)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value}%`, '湿度']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="humidity"
|
||||||
|
stroke="#2563eb"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#2563eb', r: 3 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 风速变化曲线 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">风速变化曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={weatherData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
label={{
|
||||||
|
value: '风速 (m/s)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value} m/s`, '风速']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="windSpeed"
|
||||||
|
stroke="#06b6d4"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#06b6d4', r: 3 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 降雨量统计 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">降雨量统计</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<BarChart data={weatherData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
label={{
|
||||||
|
value: '降雨量 (mm)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value} mm`, '降雨量']}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="rainfall"
|
||||||
|
fill="#6366f1"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 光照强度曲线 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">光照强度曲线</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={weatherData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
label={{
|
||||||
|
value: '光照 (W/m²)',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 12, fill: '#6b7280' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value} W/m²`, '光照强度']}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="sunlight"
|
||||||
|
stroke="#eab308"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#eab308', r: 3 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { RefreshCw } from 'lucide-react';
|
||||||
|
import { WeatherDataState, WeatherDataAction } from './weatherDataReducer';
|
||||||
|
import WeatherAlerts from './WeatherAlerts';
|
||||||
|
import RealtimeWeatherCards from './RealtimeWeatherCards';
|
||||||
|
import ControlPanel from './ControlPanel';
|
||||||
|
import WeatherCharts from './WeatherCharts';
|
||||||
|
import WeatherForecast from './WeatherForecast';
|
||||||
|
import WeatherHistory from './WeatherHistory';
|
||||||
|
import WeatherAnalysis from './WeatherAnalysis';
|
||||||
|
import UsageGuide from './UsageGuide';
|
||||||
|
|
||||||
|
interface WeatherContentProps {
|
||||||
|
state: WeatherDataState;
|
||||||
|
dispatch: React.Dispatch<WeatherDataAction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeatherContent({ state, dispatch }: WeatherContentProps) {
|
||||||
|
const handleRefreshData = async () => {
|
||||||
|
if (!state.weatherStation.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_REFRESHING', payload: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
// 生成新的测试数据
|
||||||
|
const newData = state.weatherData.map(d => ({
|
||||||
|
...d,
|
||||||
|
temperature: d.temperature + (Math.random() - 0.5) * 2,
|
||||||
|
humidity: Math.max(20, Math.min(95, d.humidity + (Math.random() - 0.5) * 5)),
|
||||||
|
windSpeed: Math.max(0, d.windSpeed + (Math.random() - 0.5) * 1),
|
||||||
|
rainfall: Math.max(0, d.rainfall + Math.random() * 2),
|
||||||
|
sunlight: Math.max(0, d.sunlight + (Math.random() - 0.5) * 50),
|
||||||
|
pressure: d.pressure + (Math.random() - 0.5) * 2,
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch({ type: 'UPDATE_WEATHER_DATA', payload: newData });
|
||||||
|
dispatch({ type: 'SET_LAST_UPDATE_TIME', payload: new Date().toLocaleString('zh-CN') });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh weather data:', error);
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_REFRESHING', payload: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 操作按钮区域 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl text-green-800 dark:text-green-200">气象监测</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
实时气象数据、历史数据分析与灾害天气预警
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleRefreshData}
|
||||||
|
disabled={state.isRefreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${state.isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
|
{state.isRefreshing ? '刷新中...' : '刷新数据'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 天气预警 */}
|
||||||
|
<WeatherAlerts state={state} dispatch={dispatch} />
|
||||||
|
|
||||||
|
{/* 实时气象数据卡片 */}
|
||||||
|
<RealtimeWeatherCards weatherData={state.weatherData} />
|
||||||
|
|
||||||
|
{/* 控制面板 */}
|
||||||
|
<ControlPanel state={state} dispatch={dispatch} />
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<Tabs value={state.activeTab} onValueChange={(value) => dispatch({ type: 'SET_ACTIVE_TAB', payload: value })}>
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="charts">数据曲线</TabsTrigger>
|
||||||
|
<TabsTrigger value="forecast">天气预报</TabsTrigger>
|
||||||
|
<TabsTrigger value="history">历史数据</TabsTrigger>
|
||||||
|
<TabsTrigger value="analysis">数据分析</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="charts" className="space-y-4">
|
||||||
|
<WeatherCharts
|
||||||
|
weatherData={state.weatherData}
|
||||||
|
timeRange={state.filters.timeRange}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="forecast" className="space-y-4">
|
||||||
|
<WeatherForecast
|
||||||
|
weatherForecast={state.weatherForecast}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="history" className="space-y-4">
|
||||||
|
<WeatherHistory
|
||||||
|
weatherData={state.weatherData}
|
||||||
|
lastUpdateTime={state.lastUpdateTime}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="analysis" className="space-y-4">
|
||||||
|
<WeatherAnalysis
|
||||||
|
statistics={state.statistics}
|
||||||
|
weatherData={state.weatherData}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* 使用说明 */}
|
||||||
|
<UsageGuide />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { WeatherForecast } from './weatherDataReducer';
|
||||||
|
import {
|
||||||
|
Sun,
|
||||||
|
Cloud,
|
||||||
|
CloudRain,
|
||||||
|
Wind,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface WeatherForecastProps {
|
||||||
|
weatherForecast: WeatherForecast[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeatherForecast({ weatherForecast }: WeatherForecastProps) {
|
||||||
|
const getWeatherIcon = (icon: string) => {
|
||||||
|
switch (icon) {
|
||||||
|
case 'sun':
|
||||||
|
return <Sun className="w-8 h-8" />;
|
||||||
|
case 'cloud':
|
||||||
|
return <Cloud className="w-8 h-8" />;
|
||||||
|
case 'rain':
|
||||||
|
return <CloudRain className="w-8 h-8" />;
|
||||||
|
case 'wind':
|
||||||
|
return <Wind className="w-8 h-8" />;
|
||||||
|
default:
|
||||||
|
return <Cloud className="w-8 h-8" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconColor = (weather: string) => {
|
||||||
|
switch (weather) {
|
||||||
|
case '晴':
|
||||||
|
return 'text-yellow-500';
|
||||||
|
case '多云':
|
||||||
|
return 'text-gray-500';
|
||||||
|
case '阴':
|
||||||
|
return 'text-gray-600';
|
||||||
|
case '小雨':
|
||||||
|
case '中雨':
|
||||||
|
case '大雨':
|
||||||
|
case '暴雨':
|
||||||
|
return 'text-blue-500';
|
||||||
|
case '大风':
|
||||||
|
return 'text-cyan-500';
|
||||||
|
default:
|
||||||
|
return 'text-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 7天天气预报卡片 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4">
|
||||||
|
{weatherForecast.map((forecast, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
className="p-4 text-center hover:bg-muted/50 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium mb-2">{forecast.day}</p>
|
||||||
|
<div className={`flex justify-center mb-2 ${getIconColor(forecast.weather)}`}>
|
||||||
|
{getWeatherIcon(forecast.icon)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">{forecast.weather}</p>
|
||||||
|
<p className="text-sm font-medium">{forecast.tempRange}</p>
|
||||||
|
{forecast.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-2 line-clamp-2">
|
||||||
|
{forecast.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 趋势分析 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-4">未来7天趋势分析</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 温度趋势图 */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3">温度变化趋势</h4>
|
||||||
|
<div className="relative h-32 border rounded-lg p-4 bg-gradient-to-b from-red-50 to-blue-50">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-red-600">18°C</div>
|
||||||
|
<div className="text-sm text-muted-foreground">平均温度</div>
|
||||||
|
<div className="text-xs text-green-600 mt-1">↑ 3°C vs 昨日</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 简化的趋势线 */}
|
||||||
|
<svg className="absolute inset-0 w-full h-full" style={{ pointerEvents: 'none' }}>
|
||||||
|
<polyline
|
||||||
|
points="10,80 50,60 90,40 130,30 170,35 210,25 250,20"
|
||||||
|
fill="none"
|
||||||
|
stroke="#dc2626"
|
||||||
|
strokeWidth="2"
|
||||||
|
opacity="0.6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3">降水概率分析</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[
|
||||||
|
{ day: '明天', probability: 20 },
|
||||||
|
{ day: '后天', probability: 80 },
|
||||||
|
{ day: '周四', probability: 10 },
|
||||||
|
{ day: '周五', probability: 30 },
|
||||||
|
].map((item, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-3">
|
||||||
|
<span className="text-xs w-12">{item.day}</span>
|
||||||
|
<div className="flex-1 h-4 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${
|
||||||
|
item.probability > 60 ? 'bg-blue-500' :
|
||||||
|
item.probability > 30 ? 'bg-blue-400' : 'bg-green-400'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${item.probability}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs w-10 text-right">{item.probability}%</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 综合天气分析 */}
|
||||||
|
<div className="p-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
|
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-3">天气综合分析</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<div>
|
||||||
|
<p><strong>温度趋势:</strong>本周气温整体呈上升趋势,周末最高温度可达28°C</p>
|
||||||
|
<p><strong>降水情况:</strong>周三有明显降水,其他时间以晴好天气为主</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p><strong>风力条件:</strong>周二风力较大,需注意防范</p>
|
||||||
|
<p><strong>农业建议:</strong>适宜进行田间作业,注意周三排水防涝</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 重要提醒 */}
|
||||||
|
<div className="p-4 bg-amber-50 dark:bg-amber-950 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||||
|
<h4 className="text-sm font-medium text-amber-900 dark:text-amber-100 mb-2">重要天气提醒</h4>
|
||||||
|
<ul className="text-sm text-amber-800 dark:text-amber-200 space-y-1">
|
||||||
|
<li>• 周二有大风天气,请加固农业设施</li>
|
||||||
|
<li>• 周三有降雨,注意提前做好排水准备</li>
|
||||||
|
<li>• 周末温度较高,注意作物防暑降温</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { WeatherData } from './weatherDataReducer';
|
||||||
|
import { Download, Search, Calendar } from 'lucide-react';
|
||||||
|
|
||||||
|
interface WeatherHistoryProps {
|
||||||
|
weatherData: WeatherData[];
|
||||||
|
lastUpdateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeatherHistory({ weatherData, lastUpdateTime }: WeatherHistoryProps) {
|
||||||
|
const handleExportData = () => {
|
||||||
|
// 生成CSV数据
|
||||||
|
const headers = ['时间', '温度(°C)', '湿度(%)', '风速(m/s)', '降雨量(mm)', '光照(W/m²)', '气压(hPa)'];
|
||||||
|
const rows = weatherData.map(data => [
|
||||||
|
data.time,
|
||||||
|
data.temperature,
|
||||||
|
data.humidity,
|
||||||
|
data.windSpeed,
|
||||||
|
data.rainfall,
|
||||||
|
data.sunlight,
|
||||||
|
data.pressure,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...rows.map(row => row.join(',')),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `气象数据_${new Date().toLocaleDateString()}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">历史气象数据查询</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索时间点..."
|
||||||
|
className="pl-10 w-48"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleExportData}>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出数据
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数据概览 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 p-4 bg-muted rounded-lg">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">{weatherData.length}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">数据点</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{Math.max(...weatherData.map(d => d.temperature))}°C
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">最高温度</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-cyan-600">
|
||||||
|
{weatherData.reduce((sum, d) => sum + d.rainfall, 0)}mm
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">总降雨量</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">
|
||||||
|
{Math.round(weatherData.reduce((sum, d) => sum + d.windSpeed, 0) / weatherData.length)}m/s
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">平均风速</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数据表格 */}
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium">时间</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-medium">温度(°C)</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-medium">湿度(%)</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-medium">风速(m/s)</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-medium">降雨量(mm)</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-medium">光照(W/m²)</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-medium">气压(hPa)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{weatherData.map((data, index) => (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
className="border-t hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar className="w-4 h-4 text-muted-foreground" />
|
||||||
|
{data.time}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<Badge
|
||||||
|
variant={data.temperature > 25 ? 'destructive' : data.temperature < 15 ? 'secondary' : 'outline'}
|
||||||
|
className="font-light"
|
||||||
|
>
|
||||||
|
{data.temperature}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<Badge
|
||||||
|
variant={data.humidity > 70 ? 'outline' : 'secondary'}
|
||||||
|
className="font-light"
|
||||||
|
>
|
||||||
|
{data.humidity}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<Badge
|
||||||
|
variant={data.windSpeed > 5 ? 'destructive' : 'outline'}
|
||||||
|
className="font-light"
|
||||||
|
>
|
||||||
|
{data.windSpeed}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
{data.rainfall > 0 ? (
|
||||||
|
<Badge variant="outline" className="font-light bg-blue-50 text-blue-700 border-blue-200">
|
||||||
|
{data.rainfall}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<Badge
|
||||||
|
variant={data.sunlight > 500 ? 'outline' : 'secondary'}
|
||||||
|
className="font-light"
|
||||||
|
>
|
||||||
|
{data.sunlight}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-sm">
|
||||||
|
{data.pressure}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{weatherData.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<Calendar className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>暂无历史数据</p>
|
||||||
|
<p className="text-sm mt-1">请等待气象数据更新</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 数据更新信息 */}
|
||||||
|
<div className="mt-4 pt-4 border-t text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>数据来源:{weatherData.length > 0 ? '自动气象站' : '暂无数据'}</span>
|
||||||
|
<span>最后更新:{lastUpdateTime || '未更新'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 数据统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="font-medium mb-3">温度分布</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">高温天数:</span>
|
||||||
|
<span className="font-medium">{weatherData.filter(d => d.temperature > 30).length} 天</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">低温天数:</span>
|
||||||
|
<span className="font-medium">{weatherData.filter(d => d.temperature < 10).length} 天</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">适宜天数:</span>
|
||||||
|
<span className="font-medium">{weatherData.filter(d => d.temperature >= 15 && d.temperature <= 25).length} 天</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="font-medium mb-3">降水统计</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">降雨天数:</span>
|
||||||
|
<span className="font-medium">{weatherData.filter(d => d.rainfall > 0).length} 天</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">最大降雨:</span>
|
||||||
|
<span className="font-medium">{Math.max(...weatherData.map(d => d.rainfall))} mm</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">降雨强度:</span>
|
||||||
|
<span className="font-medium">小雨</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="font-medium mb-3">风力条件</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">大风天数:</span>
|
||||||
|
<span className="font-medium">{weatherData.filter(d => d.windSpeed > 5).length} 天</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">静风天数:</span>
|
||||||
|
<span className="font-medium">{weatherData.filter(d => d.windSpeed < 1).length} 天</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">主导风向:</span>
|
||||||
|
<span className="font-medium">东南风</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 气象数据接口
|
||||||
|
export interface WeatherData {
|
||||||
|
time: string;
|
||||||
|
temperature: number;
|
||||||
|
humidity: number;
|
||||||
|
windSpeed: number;
|
||||||
|
rainfall: number;
|
||||||
|
sunlight: number;
|
||||||
|
pressure: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 天气预警接口
|
||||||
|
export interface WeatherAlert {
|
||||||
|
id: string;
|
||||||
|
type: '低温冻害' | '大风' | '暴雨' | '高温';
|
||||||
|
level: '预警' | '警报' | '紧急';
|
||||||
|
message: string;
|
||||||
|
time: string;
|
||||||
|
affectedFields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 天气预报接口
|
||||||
|
export interface WeatherForecast {
|
||||||
|
day: string;
|
||||||
|
weather: string;
|
||||||
|
tempRange: string;
|
||||||
|
icon: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 气象站配置接口
|
||||||
|
export interface WeatherStationConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
apiEndpoint: string;
|
||||||
|
apiKey: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
stationName: string;
|
||||||
|
updateInterval: number; // 分钟
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选条件接口
|
||||||
|
export interface WeatherFilters {
|
||||||
|
selectedField: string;
|
||||||
|
timeRange: '24h' | '7d' | '30d';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计分析结果接口
|
||||||
|
export interface WeatherStatistics {
|
||||||
|
temperature: {
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
avg: number;
|
||||||
|
range: number;
|
||||||
|
};
|
||||||
|
precipitation: {
|
||||||
|
total: number;
|
||||||
|
maxHourly: number;
|
||||||
|
timeRange: string;
|
||||||
|
intensity: string;
|
||||||
|
};
|
||||||
|
wind: {
|
||||||
|
maxSpeed: number;
|
||||||
|
avgSpeed: number;
|
||||||
|
level: string;
|
||||||
|
direction: string;
|
||||||
|
};
|
||||||
|
other: {
|
||||||
|
avgHumidity: number;
|
||||||
|
totalSunlight: number;
|
||||||
|
avgPressure: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态接口
|
||||||
|
export interface WeatherDataState {
|
||||||
|
weatherData: WeatherData[];
|
||||||
|
weatherAlerts: WeatherAlert[];
|
||||||
|
weatherForecast: WeatherForecast[];
|
||||||
|
weatherStation: WeatherStationConfig;
|
||||||
|
filters: WeatherFilters;
|
||||||
|
statistics: WeatherStatistics | null;
|
||||||
|
activeTab: string;
|
||||||
|
acknowledgedAlerts: string[];
|
||||||
|
isRefreshing: boolean;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action类型
|
||||||
|
export type WeatherDataAction =
|
||||||
|
| { type: 'SET_WEATHER_DATA'; payload: WeatherData[] }
|
||||||
|
| { type: 'SET_WEATHER_ALERTS'; payload: WeatherAlert[] }
|
||||||
|
| { type: 'SET_WEATHER_FORECAST'; payload: WeatherForecast[] }
|
||||||
|
| { type: 'SET_WEATHER_STATION'; payload: WeatherStationConfig }
|
||||||
|
| { type: 'SET_FILTERS'; payload: Partial<WeatherFilters> }
|
||||||
|
| { type: 'SET_STATISTICS'; payload: WeatherStatistics | null }
|
||||||
|
| { type: 'SET_ACTIVE_TAB'; payload: string }
|
||||||
|
| { type: 'ACKNOWLEDGE_ALERT'; payload: string }
|
||||||
|
| { type: 'ACKNOWLEDGE_ALL_ALERTS' }
|
||||||
|
| { type: 'SET_REFRESHING'; payload: boolean }
|
||||||
|
| { type: 'SET_LAST_UPDATE_TIME'; payload: string }
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'ADD_WEATHER_ALERT'; payload: WeatherAlert }
|
||||||
|
| { type: 'REMOVE_WEATHER_ALERT'; payload: string }
|
||||||
|
| { type: 'UPDATE_WEATHER_DATA'; payload: WeatherData[] }
|
||||||
|
| { type: 'LOAD_FROM_STORAGE' }
|
||||||
|
| { type: 'SAVE_TO_STORAGE' };
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
export const initialWeatherDataState: WeatherDataState = {
|
||||||
|
weatherData: [],
|
||||||
|
weatherAlerts: [],
|
||||||
|
weatherForecast: [],
|
||||||
|
weatherStation: {
|
||||||
|
enabled: true,
|
||||||
|
apiEndpoint: 'https://api.weather.example.com/data',
|
||||||
|
apiKey: 'YOUR_API_KEY_HERE',
|
||||||
|
latitude: 39.9042,
|
||||||
|
longitude: 116.4074,
|
||||||
|
stationName: '智慧农场气象站',
|
||||||
|
updateInterval: 15,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
selectedField: 'field-1',
|
||||||
|
timeRange: '24h',
|
||||||
|
},
|
||||||
|
statistics: null,
|
||||||
|
activeTab: 'charts',
|
||||||
|
acknowledgedAlerts: [],
|
||||||
|
isRefreshing: false,
|
||||||
|
lastUpdateTime: '',
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成测试数据
|
||||||
|
const generateTestData = (timeRange: '24h' | '7d' | '30d'): WeatherData[] => {
|
||||||
|
if (timeRange === '24h') {
|
||||||
|
// 24小时数据 - 每3小时一个数据点
|
||||||
|
return [
|
||||||
|
{ time: '00:00', temperature: 12, humidity: 75, windSpeed: 2.5, rainfall: 0, sunlight: 0, pressure: 1013 },
|
||||||
|
{ time: '03:00', temperature: 10, humidity: 80, windSpeed: 2.0, rainfall: 0, sunlight: 0, pressure: 1012 },
|
||||||
|
{ time: '06:00', temperature: 9, humidity: 85, windSpeed: 1.8, rainfall: 0, sunlight: 50, pressure: 1012 },
|
||||||
|
{ time: '09:00', temperature: 15, humidity: 70, windSpeed: 3.2, rainfall: 0, sunlight: 450, pressure: 1013 },
|
||||||
|
{ time: '12:00', temperature: 22, humidity: 55, windSpeed: 4.5, rainfall: 0, sunlight: 850, pressure: 1014 },
|
||||||
|
{ time: '15:00', temperature: 25, humidity: 48, windSpeed: 5.2, rainfall: 0, sunlight: 680, pressure: 1013 },
|
||||||
|
{ time: '18:00', temperature: 20, humidity: 60, windSpeed: 3.8, rainfall: 2, sunlight: 150, pressure: 1012 },
|
||||||
|
{ time: '21:00', temperature: 16, humidity: 68, windSpeed: 3.0, rainfall: 5, sunlight: 0, pressure: 1011 },
|
||||||
|
];
|
||||||
|
} else if (timeRange === '7d') {
|
||||||
|
// 7天数据 - 每天一个数据点
|
||||||
|
return [
|
||||||
|
{ time: '10-12', temperature: 18, humidity: 68, windSpeed: 3.2, rainfall: 2, sunlight: 520, pressure: 1012 },
|
||||||
|
{ time: '10-13', temperature: 20, humidity: 62, windSpeed: 4.1, rainfall: 0, sunlight: 680, pressure: 1013 },
|
||||||
|
{ time: '10-14', temperature: 22, humidity: 58, windSpeed: 3.8, rainfall: 5, sunlight: 720, pressure: 1014 },
|
||||||
|
{ time: '10-15', temperature: 19, humidity: 72, windSpeed: 5.5, rainfall: 12, sunlight: 450, pressure: 1011 },
|
||||||
|
{ time: '10-16', temperature: 16, humidity: 78, windSpeed: 4.8, rainfall: 8, sunlight: 380, pressure: 1010 },
|
||||||
|
{ time: '10-17', temperature: 21, humidity: 65, windSpeed: 3.5, rainfall: 0, sunlight: 750, pressure: 1013 },
|
||||||
|
{ time: '10-18', temperature: 23, humidity: 60, windSpeed: 3.0, rainfall: 0, sunlight: 800, pressure: 1015 },
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// 30天数据 - 每5天一个数据点
|
||||||
|
return [
|
||||||
|
{ time: '09-19', temperature: 15, humidity: 70, windSpeed: 3.5, rainfall: 15, sunlight: 480, pressure: 1011 },
|
||||||
|
{ time: '09-24', temperature: 17, humidity: 68, windSpeed: 4.0, rainfall: 8, sunlight: 550, pressure: 1012 },
|
||||||
|
{ time: '09-29', temperature: 19, humidity: 65, windSpeed: 3.8, rainfall: 5, sunlight: 620, pressure: 1013 },
|
||||||
|
{ time: '10-04', temperature: 21, humidity: 62, windSpeed: 4.2, rainfall: 3, sunlight: 680, pressure: 1014 },
|
||||||
|
{ time: '10-09', temperature: 20, humidity: 66, windSpeed: 3.9, rainfall: 10, sunlight: 600, pressure: 1012 },
|
||||||
|
{ time: '10-14', temperature: 22, humidity: 58, windSpeed: 3.5, rainfall: 2, sunlight: 720, pressure: 1015 },
|
||||||
|
{ time: '10-18', temperature: 23, humidity: 60, windSpeed: 3.0, rainfall: 0, sunlight: 800, pressure: 1015 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成测试预警数据
|
||||||
|
const generateTestAlerts = (): WeatherAlert[] => [
|
||||||
|
{
|
||||||
|
id: 'alert-1',
|
||||||
|
type: '大风',
|
||||||
|
level: '预警',
|
||||||
|
message: '预计明日下午将出现6-7级大风,请做好防范措施',
|
||||||
|
time: '2024-10-15 14:30',
|
||||||
|
affectedFields: ['东区1号地', '西区2号地'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alert-2',
|
||||||
|
type: '低温冻害',
|
||||||
|
level: '警报',
|
||||||
|
message: '预计后天凌晨最低温度将降至2℃,注意防冻',
|
||||||
|
time: '2024-10-15 10:20',
|
||||||
|
affectedFields: ['南区3号地'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 生成测试预报数据
|
||||||
|
const generateTestForecast = (): WeatherForecast[] => [
|
||||||
|
{ day: '今天', weather: '多云', tempRange: '12-25°C', icon: 'cloud', description: '多云天气,温度适宜' },
|
||||||
|
{ day: '明天', weather: '大风', tempRange: '10-22°C', icon: 'wind', description: '风力较大,注意防范' },
|
||||||
|
{ day: '后天', weather: '小雨', tempRange: '8-18°C', icon: 'rain', description: '有小雨,携带雨具' },
|
||||||
|
{ day: '周四', weather: '晴', tempRange: '12-26°C', icon: 'sun', description: '晴朗天气,光照充足' },
|
||||||
|
{ day: '周五', weather: '多云', tempRange: '14-27°C', icon: 'cloud', description: '多云,温度上升' },
|
||||||
|
{ day: '周六', weather: '晴', tempRange: '15-28°C', icon: 'sun', description: '晴天,适宜户外活动' },
|
||||||
|
{ day: '周日', weather: '多云', tempRange: '13-25°C', icon: 'cloud', description: '多云,天气稳定' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const calculateStatistics = (data: WeatherData[]): WeatherStatistics => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
return {
|
||||||
|
temperature: { max: 0, min: 0, avg: 0, range: 0 },
|
||||||
|
precipitation: { total: 0, maxHourly: 0, timeRange: '', intensity: '无' },
|
||||||
|
wind: { maxSpeed: 0, avgSpeed: 0, level: '无风', direction: '无' },
|
||||||
|
other: { avgHumidity: 0, totalSunlight: 0, avgPressure: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const temperatures = data.map(d => d.temperature);
|
||||||
|
const maxTemp = Math.max(...temperatures);
|
||||||
|
const minTemp = Math.min(...temperatures);
|
||||||
|
const avgTemp = temperatures.reduce((sum, t) => sum + t, 0) / temperatures.length;
|
||||||
|
|
||||||
|
const totalRainfall = data.reduce((sum, d) => sum + d.rainfall, 0);
|
||||||
|
const maxRainfall = Math.max(...data.map(d => d.rainfall));
|
||||||
|
|
||||||
|
const windSpeeds = data.map(d => d.windSpeed);
|
||||||
|
const maxWindSpeed = Math.max(...windSpeeds);
|
||||||
|
const avgWindSpeed = windSpeeds.reduce((sum, w) => sum + w, 0) / windSpeeds.length;
|
||||||
|
|
||||||
|
const avgHumidity = data.reduce((sum, d) => sum + d.humidity, 0) / data.length;
|
||||||
|
const totalSunlight = data.reduce((sum, d) => sum + d.sunlight, 0);
|
||||||
|
const avgPressure = data.reduce((sum, d) => sum + d.pressure, 0) / data.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
temperature: {
|
||||||
|
max: maxTemp,
|
||||||
|
min: minTemp,
|
||||||
|
avg: Math.round(avgTemp * 10) / 10,
|
||||||
|
range: maxTemp - minTemp,
|
||||||
|
},
|
||||||
|
precipitation: {
|
||||||
|
total: totalRainfall,
|
||||||
|
maxHourly: maxRainfall,
|
||||||
|
timeRange: '18:00-21:00',
|
||||||
|
intensity: totalRainfall > 50 ? '暴雨' : totalRainfall > 25 ? '大雨' : totalRainfall > 10 ? '中雨' : totalRainfall > 0 ? '小雨' : '无',
|
||||||
|
},
|
||||||
|
wind: {
|
||||||
|
maxSpeed: Math.round(maxWindSpeed * 10) / 10,
|
||||||
|
avgSpeed: Math.round(avgWindSpeed * 10) / 10,
|
||||||
|
level: maxWindSpeed > 10 ? '5-6级' : maxWindSpeed > 5 ? '3-4级' : '1-2级',
|
||||||
|
direction: '东南风',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
avgHumidity: Math.round(avgHumidity),
|
||||||
|
totalSunlight,
|
||||||
|
avgPressure: Math.round(avgPressure),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检测灾害天气
|
||||||
|
const checkWeatherAlerts = (data: WeatherData[]): WeatherAlert[] => {
|
||||||
|
const alerts: WeatherAlert[] = [];
|
||||||
|
|
||||||
|
data.forEach((d) => {
|
||||||
|
// 低温冻害预警(温度低于3°C)
|
||||||
|
if (d.temperature <= 3) {
|
||||||
|
alerts.push({
|
||||||
|
id: `alert-freeze-${d.time}`,
|
||||||
|
type: '低温冻害',
|
||||||
|
level: '警报',
|
||||||
|
message: `预计${d.time}温度将降至${d.temperature}°C,请做好防冻措施`,
|
||||||
|
time: new Date().toLocaleString('zh-CN'),
|
||||||
|
affectedFields: ['东区1号地', '西区2号地', '南区3号地'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大风预警(风速大于10m/s)
|
||||||
|
if (d.windSpeed >= 10) {
|
||||||
|
alerts.push({
|
||||||
|
id: `alert-wind-${d.time}`,
|
||||||
|
type: '大风',
|
||||||
|
level: '预警',
|
||||||
|
message: `预计${d.time}风速将达到${d.windSpeed}m/s,请加固设施`,
|
||||||
|
time: new Date().toLocaleString('zh-CN'),
|
||||||
|
affectedFields: ['东区1号地', '西区2号地'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴雨预警(3小时降雨量大于50mm)
|
||||||
|
if (d.rainfall >= 50) {
|
||||||
|
alerts.push({
|
||||||
|
id: `alert-rain-${d.time}`,
|
||||||
|
type: '暴雨',
|
||||||
|
level: '紧急',
|
||||||
|
message: `预计${d.time}降雨量将达到${d.rainfall}mm,请注意排涝`,
|
||||||
|
time: new Date().toLocaleString('zh-CN'),
|
||||||
|
affectedFields: ['南区3号地'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高温预警(温度高于35°C)
|
||||||
|
if (d.temperature >= 35) {
|
||||||
|
alerts.push({
|
||||||
|
id: `alert-heat-${d.time}`,
|
||||||
|
type: '高温',
|
||||||
|
level: '预警',
|
||||||
|
message: `预计${d.time}温度将达到${d.temperature}°C,注意防暑降温`,
|
||||||
|
time: new Date().toLocaleString('zh-CN'),
|
||||||
|
affectedFields: ['东区1号地'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return alerts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reducer函数
|
||||||
|
export function weatherDataReducer(state: WeatherDataState, action: WeatherDataAction): WeatherDataState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_WEATHER_DATA':
|
||||||
|
const statistics = calculateStatistics(action.payload);
|
||||||
|
const newAlerts = checkWeatherAlerts(action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
weatherData: action.payload,
|
||||||
|
statistics,
|
||||||
|
weatherAlerts: [...state.weatherAlerts, ...newAlerts],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_WEATHER_ALERTS':
|
||||||
|
return { ...state, weatherAlerts: action.payload };
|
||||||
|
|
||||||
|
case 'SET_WEATHER_FORECAST':
|
||||||
|
return { ...state, weatherForecast: action.payload };
|
||||||
|
|
||||||
|
case 'SET_WEATHER_STATION':
|
||||||
|
return { ...state, weatherStation: action.payload };
|
||||||
|
|
||||||
|
case 'SET_FILTERS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: { ...state.filters, ...action.payload },
|
||||||
|
// 重新生成对应时间范围的数据
|
||||||
|
weatherData: generateTestData(action.payload.timeRange || state.filters.timeRange),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_STATISTICS':
|
||||||
|
return { ...state, statistics: action.payload };
|
||||||
|
|
||||||
|
case 'SET_ACTIVE_TAB':
|
||||||
|
return { ...state, activeTab: action.payload };
|
||||||
|
|
||||||
|
case 'ACKNOWLEDGE_ALERT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
acknowledgedAlerts: [...state.acknowledgedAlerts, action.payload],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'ACKNOWLEDGE_ALL_ALERTS':
|
||||||
|
const allAlertIds = state.weatherAlerts.map(alert => alert.id);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
acknowledgedAlerts: allAlertIds,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_REFRESHING':
|
||||||
|
return { ...state, isRefreshing: action.payload };
|
||||||
|
|
||||||
|
case 'SET_LAST_UPDATE_TIME':
|
||||||
|
return { ...state, lastUpdateTime: action.payload };
|
||||||
|
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return { ...state, loading: action.payload };
|
||||||
|
|
||||||
|
case 'ADD_WEATHER_ALERT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
weatherAlerts: [action.payload, ...state.weatherAlerts],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'REMOVE_WEATHER_ALERT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
weatherAlerts: state.weatherAlerts.filter(alert => alert.id !== action.payload),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_WEATHER_DATA':
|
||||||
|
const updatedStatistics = calculateStatistics(action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
weatherData: action.payload,
|
||||||
|
statistics: updatedStatistics,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'LOAD_FROM_STORAGE':
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('weatherData');
|
||||||
|
if (stored) {
|
||||||
|
const parsedData = JSON.parse(stored);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...parsedData,
|
||||||
|
weatherData: parsedData.weatherData || generateTestData('24h'),
|
||||||
|
weatherAlerts: parsedData.weatherAlerts || generateTestAlerts(),
|
||||||
|
weatherForecast: parsedData.weatherForecast || generateTestForecast(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 首次加载,初始化测试数据
|
||||||
|
const initialData = {
|
||||||
|
weatherData: generateTestData('24h'),
|
||||||
|
weatherAlerts: generateTestAlerts(),
|
||||||
|
weatherForecast: generateTestForecast(),
|
||||||
|
lastUpdateTime: new Date().toLocaleString('zh-CN'),
|
||||||
|
};
|
||||||
|
localStorage.setItem('weatherData', JSON.stringify(initialData));
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...initialData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load weather data from storage:', error);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SAVE_TO_STORAGE':
|
||||||
|
try {
|
||||||
|
const dataToSave = {
|
||||||
|
weatherStation: state.weatherStation,
|
||||||
|
filters: state.filters,
|
||||||
|
weatherData: state.weatherData,
|
||||||
|
weatherAlerts: state.weatherAlerts,
|
||||||
|
weatherForecast: state.weatherForecast,
|
||||||
|
lastUpdateTime: state.lastUpdateTime,
|
||||||
|
};
|
||||||
|
localStorage.setItem('weatherData', JSON.stringify(dataToSave));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save weather data to storage:', error);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
export const getAlertColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case '预警': return 'bg-yellow-500';
|
||||||
|
case '警报': return 'bg-orange-500';
|
||||||
|
case '紧急': return 'bg-red-500';
|
||||||
|
default: return 'bg-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWeatherIcon = (weather: string) => {
|
||||||
|
switch (weather) {
|
||||||
|
case '晴': return 'sun';
|
||||||
|
case '多云': return 'cloud';
|
||||||
|
case '阴': return 'cloud';
|
||||||
|
case '小雨': return 'rain';
|
||||||
|
case '中雨': return 'rain';
|
||||||
|
case '大雨': return 'rain';
|
||||||
|
case '暴雨': return 'rain';
|
||||||
|
case '大风': return 'wind';
|
||||||
|
default: return 'cloud';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatTemperature = (temp: number) => `${temp}°C`;
|
||||||
|
export const formatWindSpeed = (speed: number) => `${speed} m/s`;
|
||||||
|
export const formatRainfall = (rainfall: number) => `${rainfall} mm`;
|
||||||
|
export const formatSunlight = (sunlight: number) => `${sunlight} W/m²`;
|
||||||
|
export const formatPressure = (pressure: number) => `${pressure} hPa`;
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer, useEffect } from 'react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { weatherDataReducer, initialWeatherDataState, WeatherDataState } from './components/weatherDataReducer';
|
||||||
|
import WeatherContent from './components/WeatherContent';
|
||||||
|
|
||||||
export default function WeatherPage() {
|
export default function WeatherPage() {
|
||||||
|
const [state, dispatch] = useReducer(weatherDataReducer, initialWeatherDataState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 加载存储的数据
|
||||||
|
dispatch({ type: 'LOAD_FROM_STORAGE' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 保存数据到localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'SAVE_TO_STORAGE' });
|
||||||
|
}, [state.weatherData, state.weatherAlerts, state.filters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
|
||||||
<h2 className="text-xl font-semibold">气象监测</h2>
|
<WeatherContent state={state} dispatch={dispatch} />
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
|
||||||
<p className="text-sm">
|
|
||||||
<strong>页面路径:</strong> /land-information/monitoring/weather
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user