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