Files
smart-cropx-ui/src/app/(app)/land-information/suitability/auto/page.tsx

458 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>: 7pH值</li>
<li> <strong>3. </strong>: 0-100</li>
<li> <strong>4. </strong>: 使=100% = Σ( × )</li>
<li> <strong>5. </strong>: (80)(60-79)(&lt;60)</li>
<li> <strong>6. </strong>: </li>
<li> <strong>7. </strong>: </li>
</ul>
</div>
</div>
</Card>
</div>
);
}