416 lines
17 KiB
TypeScript
416 lines
17 KiB
TypeScript
/**
|
||
* 多因子综合评价组件
|
||
* 提供地块适宜性评价、权重配置、批量分析和作物推荐功能
|
||
*/
|
||
|
||
'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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '@/components/ui/dialog';
|
||
import { Progress } from '@/components/ui/progress';
|
||
import { Slider } from '@/components/ui/slider';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import {
|
||
Leaf,
|
||
TrendingUp,
|
||
Award,
|
||
AlertTriangle,
|
||
CheckCircle2,
|
||
Play,
|
||
Settings,
|
||
Download,
|
||
Eye,
|
||
Calculator,
|
||
Database,
|
||
RefreshCw,
|
||
Zap,
|
||
Target,
|
||
Droplet,
|
||
Cloud,
|
||
Sun,
|
||
ThermometerSun,
|
||
BookOpen,
|
||
Beaker,
|
||
Info,
|
||
BarChart3,
|
||
Filter
|
||
} from 'lucide-react';
|
||
import { toast } from 'sonner';
|
||
|
||
import {
|
||
EvaluationFactor,
|
||
SuitabilityResult,
|
||
FactorWeight,
|
||
BatchProgress,
|
||
getGradeColor,
|
||
getScoreColor,
|
||
getSuitabilityLevelColor,
|
||
formatDate,
|
||
MOCK_FIELDS
|
||
} from './multiFactorTypes';
|
||
import {
|
||
MultiFactorService
|
||
} from './multiFactorService';
|
||
import {
|
||
matchCropsForField,
|
||
cropKnowledgeBase
|
||
} from './cropKnowledgeBase';
|
||
|
||
export function MultiFactorEvaluation() {
|
||
const [selectedField, setSelectedField] = useState('field-1');
|
||
const [showWeightConfig, setShowWeightConfig] = useState(false);
|
||
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
|
||
const [batchProgress, setBatchProgress] = useState<BatchProgress>({
|
||
total: 0,
|
||
processed: 0,
|
||
highSuitability: 0,
|
||
mediumSuitability: 0,
|
||
lowSuitability: 0,
|
||
currentField: '',
|
||
});
|
||
const [isBatchRunning, setIsBatchRunning] = useState(false);
|
||
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
|
||
|
||
// 评价因子权重配置
|
||
const [factorWeights, setFactorWeights] = useState<FactorWeight[]>([
|
||
{ id: 'ph', name: 'pH值', weight: 20, unit: '' },
|
||
{ id: 'organic', name: '有机质含量', weight: 25, unit: 'g/kg' },
|
||
{ id: 'depth', name: '土层厚度', weight: 20, unit: 'cm' },
|
||
{ id: 'nitrogen', name: '全氮', weight: 10, unit: 'g/kg' },
|
||
{ id: 'phosphorus', name: '全磷', weight: 10, unit: 'g/kg' },
|
||
{ id: 'potassium', name: '全钾', weight: 10, unit: 'g/kg' },
|
||
{ id: 'drainage', name: '排水性', weight: 5, unit: '' },
|
||
]);
|
||
|
||
// 模拟适宜性评价结果
|
||
const [evaluationResults, setEvaluationResults] = useState<SuitabilityResult[]>(
|
||
MultiFactorService.generateMockEvaluationData()
|
||
);
|
||
|
||
// 获取当前选中的地块结果
|
||
const currentResult =
|
||
evaluationResults.find(r => r.fieldId === selectedField) ||
|
||
batchAnalysisResults.find(r => r.fieldId === selectedField) ||
|
||
evaluationResults[0];
|
||
|
||
// 批量分析处理函数
|
||
const handleRunBatchAnalysis = async () => {
|
||
const validation = MultiFactorService.validateWeights(factorWeights);
|
||
if (!validation.isValid) {
|
||
toast.error(`权重总和必须为100%才能进行批量分析(当前:${validation.totalWeight}%)`);
|
||
return;
|
||
}
|
||
|
||
setIsBatchRunning(true);
|
||
setBatchProgress({
|
||
total: 68,
|
||
processed: 0,
|
||
highSuitability: 0,
|
||
mediumSuitability: 0,
|
||
lowSuitability: 0,
|
||
currentField: '',
|
||
});
|
||
setBatchAnalysisResults([]);
|
||
|
||
toast.success('开始批量分析,正在读取地块数据...');
|
||
|
||
try {
|
||
const results = await MultiFactorService.runBatchAnalysis(
|
||
factorWeights,
|
||
(progress) => {
|
||
setBatchProgress(progress);
|
||
}
|
||
);
|
||
|
||
setBatchAnalysisResults(results);
|
||
setIsBatchRunning(false);
|
||
toast.success(`批量分析完成!已为${results.length}个地块生成适宜性评价结果`);
|
||
} catch (error) {
|
||
setIsBatchRunning(false);
|
||
toast.error('批量分析失败');
|
||
}
|
||
};
|
||
|
||
const handleUpdateWeight = (id: string, newWeight: number) => {
|
||
setFactorWeights(prev =>
|
||
MultiFactorService.updateWeight(prev, id, newWeight)
|
||
);
|
||
};
|
||
|
||
const handleResetWeights = () => {
|
||
setFactorWeights(MultiFactorService.resetWeights());
|
||
toast.success('权重已恢复默认值');
|
||
};
|
||
|
||
const handleApplyPreset = (preset: 'grain' | 'economic' | 'default') => {
|
||
setFactorWeights(MultiFactorService.getWeightPreset(preset));
|
||
const presetName = preset === 'grain' ? '粮食作物' : preset === 'economic' ? '经济作物' : '默认';
|
||
toast.success(`已应用${presetName}权重方案`);
|
||
};
|
||
|
||
const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
|
||
|
||
// 执行地块适宜性评价
|
||
const handleEvaluateField = () => {
|
||
const validation = MultiFactorService.validateWeights(factorWeights);
|
||
if (!validation.isValid) {
|
||
toast.error(`权重总和必须为100%才能进行评价(当前:${validation.totalWeight}%)`);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = MultiFactorService.generateEvaluationResult(selectedField, factorWeights);
|
||
setEvaluationResults(prev =>
|
||
prev.map(r => r.fieldId === selectedField ? result : r)
|
||
);
|
||
toast.success('评价完成!已应用当前权重配置计算综合得分');
|
||
} catch (error) {
|
||
toast.error('评价失败');
|
||
}
|
||
};
|
||
|
||
const exportResults = () => {
|
||
const resultsToExport = batchAnalysisResults.length > 0 ? batchAnalysisResults : evaluationResults;
|
||
toast.success('正在导出评价结果...');
|
||
// 模拟导出功能
|
||
setTimeout(() => {
|
||
toast.success(`已导出${resultsToExport.length}个地块的评价结果`);
|
||
}, 2000);
|
||
};
|
||
|
||
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={() => setShowKnowledgeBase(true)}>
|
||
<BookOpen className="w-4 h-4 mr-2" />
|
||
作物知识库
|
||
</Button>
|
||
<Button variant="outline" onClick={() => setShowWeightConfig(true)}>
|
||
<Settings className="w-4 h-4 mr-2" />
|
||
权重配置
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 多因子综合评价 */}
|
||
<div className="space-y-4">
|
||
<Card className="p-4 bg-card">
|
||
<div className="flex items-center gap-4">
|
||
<div className="flex-1">
|
||
<label className="text-xs text-muted-foreground mb-2 block">选择地块</label>
|
||
<Select value={selectedField} onValueChange={setSelectedField}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{evaluationResults.map((result) => (
|
||
<SelectItem key={result.fieldId} value={result.fieldId}>
|
||
{result.fieldName}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="flex gap-2 items-end">
|
||
<Button
|
||
className="bg-green-600 hover:bg-green-700"
|
||
onClick={handleEvaluateField}
|
||
>
|
||
<Play className="w-4 h-4 mr-2" />
|
||
开始评价
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 评价结果总览 */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<Card className="p-6 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900">
|
||
<div className="text-center">
|
||
<Award className="w-12 h-12 text-green-600 dark:text-green-400 mx-auto mb-3" />
|
||
<p className="text-xs text-muted-foreground mb-2">综合评分</p>
|
||
<p
|
||
className="text-4xl mb-2"
|
||
style={{ color: getGradeColor(currentResult.grade) }}
|
||
>
|
||
{currentResult.totalScore}
|
||
</p>
|
||
<Badge
|
||
className="text-white font-light"
|
||
style={{ backgroundColor: getGradeColor(currentResult.grade) }}
|
||
>
|
||
{currentResult.grade}
|
||
</Badge>
|
||
</div>
|
||
</Card>
|
||
|
||
<Card className="p-6 bg-card">
|
||
<div className="text-center">
|
||
<CheckCircle2 className="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
|
||
<p className="text-xs text-muted-foreground mb-2">优秀因子</p>
|
||
<p className="text-4xl text-blue-600 dark:text-blue-400 mb-2">
|
||
{currentResult.factors.filter(f => f.score >= 80).length}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
/ {currentResult.factors.length} 项
|
||
</p>
|
||
</div>
|
||
</Card>
|
||
|
||
<Card className="p-6 bg-card">
|
||
<div className="text-center">
|
||
<AlertTriangle className="w-12 h-12 text-yellow-600 dark:text-yellow-400 mx-auto mb-3" />
|
||
<p className="text-xs text-muted-foreground mb-2">待改善因子</p>
|
||
<p className="text-4xl text-yellow-600 dark:text-yellow-400 mb-2">
|
||
{currentResult.factors.filter(f => f.score < 70).length}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">需要优化</p>
|
||
</div>
|
||
</Card>
|
||
|
||
<Card className="p-6 bg-card">
|
||
<div className="text-center">
|
||
<TrendingUp className="w-12 h-12 text-purple-600 dark:text-purple-400 mx-auto mb-3" />
|
||
<p className="text-xs text-muted-foreground mb-2">评价时间</p>
|
||
<p className="text-sm text-purple-600 dark:text-purple-400 mb-2">
|
||
{formatDate(currentResult.timestamp)}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">最近更新</p>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 因子详细评分 */}
|
||
<Card className="p-6 bg-card">
|
||
<h3 className="mb-4">评价因子详细分析</h3>
|
||
<div className="space-y-4">
|
||
{currentResult.factors.map((factor) => (
|
||
<div key={factor.id} className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-sm font-medium">{factor.name}</span>
|
||
<Badge variant="outline" className="text-xs font-light">
|
||
权重: {factor.weight}%
|
||
</Badge>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<span className="text-sm text-muted-foreground">
|
||
实际值: {factor.value.toFixed(1)}{factor.unit}
|
||
</span>
|
||
<span className="text-sm text-muted-foreground">
|
||
最佳范围: {factor.optimalRange[0]}-{factor.optimalRange[1]}{factor.unit}
|
||
</span>
|
||
<span
|
||
className="text-sm font-medium"
|
||
style={{ color: getScoreColor(factor.score) }}
|
||
>
|
||
{factor.score}分
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="relative">
|
||
<Progress value={factor.score} className="h-2" />
|
||
<div className="absolute top-0 left-0 h-2 w-full flex items-center">
|
||
<div
|
||
className="absolute h-3 w-1 bg-blue-500"
|
||
style={{
|
||
left: `${((factor.optimalRange[0] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
|
||
}}
|
||
/>
|
||
<div
|
||
className="absolute h-3 w-1 bg-blue-500"
|
||
style={{
|
||
left: `${((factor.optimalRange[1] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 加权计算说明 */}
|
||
<Card className="p-6 bg-card">
|
||
<h3 className="mb-4">层次分析法(AHP)加权计算</h3>
|
||
<div className="space-y-4">
|
||
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||
<p className="text-sm text-blue-900 dark:text-blue-100 mb-2">计算公式:</p>
|
||
<code className="text-xs text-blue-800 dark:text-blue-200 block mb-2">
|
||
总分 = Σ(因子得分 × 因子权重)
|
||
</code>
|
||
<p className="text-xs text-blue-800 dark:text-blue-200">
|
||
= ({currentResult.factors.map((f, i) =>
|
||
`${f.score} × ${f.weight}%${i < currentResult.factors.length - 1 ? ' + ' : ''}`
|
||
).join('')})
|
||
</p>
|
||
<p className="text-xs text-blue-800 dark:text-blue-200 mt-2">
|
||
= {currentResult.totalScore}分
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div className="p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||
<h4 className="text-green-900 dark:text-green-100 mb-2">高度适宜 (≥80分)</h4>
|
||
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
|
||
<li>• 土壤条件优秀</li>
|
||
<li>• 适合多种作物</li>
|
||
<li>• 预期产量高</li>
|
||
</ul>
|
||
</div>
|
||
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
|
||
<h4 className="text-yellow-900 dark:text-yellow-100 mb-2">一般适宜 (60-79分)</h4>
|
||
<ul className="text-sm text-yellow-800 dark:text-yellow-200 space-y-1">
|
||
<li>• 部分因子需改善</li>
|
||
<li>• 可种植耐性作物</li>
|
||
<li>• 需适当改良</li>
|
||
</ul>
|
||
</div>
|
||
<div className="p-4 bg-red-50 dark:bg-red-950 rounded-lg">
|
||
<h4 className="text-red-900 dark:text-red-100 mb-2">不适宜 (<60分)</h4>
|
||
<ul className="text-sm text-red-800 dark:text-red-200 space-y-1">
|
||
<li>• 土壤条件较差</li>
|
||
<li>• 需大幅改良</li>
|
||
<li>• 风险较高</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 多因子综合评价功能说明 */}
|
||
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||
<div className="flex items-start gap-2">
|
||
<Zap className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||
<p className="mb-2">多因子综合评价功能说明:</p>
|
||
<ul className="space-y-1 text-xs">
|
||
<li>• <strong>关键指标整合</strong>: 整合pH值、有机质含量、土层厚度、全氮、全磷、全钾、排水性等7项关键土壤指标</li>
|
||
<li>• <strong>层次分析法(AHP)</strong>: 采用层次分析法构建加权评分体系,公式:总分 = Σ(因子得分 × 因子权重)</li>
|
||
<li>• <strong>单因子评分</strong>: 根据实际值与最佳范围的接近程度计算各因子得分(0-100分),范围内得分85-100分</li>
|
||
<li>• <strong>自定义权重</strong>: 支持手动调整各指标权重,提供粮食作物和经济作物两种预设方案</li>
|
||
<li>• <strong>综合得分</strong>: 输出0-100分的适宜性评分,自动计算加权总分</li>
|
||
<li>• <strong>三级分级</strong>: 高度适宜(≥80分)、一般适宜(60-79分)、不适宜(<60分)</li>
|
||
<li>• <strong>可视化展示</strong>: 提供进度条、颜色标识等多维度可视化,直观展示各因子评分与最佳范围对比</li>
|
||
<li>• <strong>改善建议</strong>: 自动识别待改善因子,为土壤改良提供决策依据</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
<Card className="p-4 bg-card">
|
||
<div className="flex items-center gap-4">
|