子仓库提交
This commit is contained in:
459
src/app/(app)/land-information/suitability/auto/page.tsx
Normal file
459
src/app/(app)/land-information/suitability/auto/page.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
'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,
|
||||
Play,
|
||||
Download,
|
||||
Eye,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface EvaluationFactor {
|
||||
id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
weight: number;
|
||||
unit: string;
|
||||
optimalRange: [number, number];
|
||||
score: number;
|
||||
}
|
||||
|
||||
interface SuitabilityResult {
|
||||
fieldId: string;
|
||||
fieldName: string;
|
||||
totalScore: number;
|
||||
grade: '高度适宜' | '一般适宜' | '不适宜';
|
||||
factors: EvaluationFactor[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export default function BatchEvaluationPage() {
|
||||
const [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 = (fieldId: string): EvaluationFactor[] => {
|
||||
return [
|
||||
{
|
||||
id: 'ph',
|
||||
name: 'pH值',
|
||||
value: 6.0 + Math.random() * 2,
|
||||
weight: factorWeights.find(f => f.id === 'ph')?.weight || 20,
|
||||
unit: '',
|
||||
optimalRange: [6.5, 7.5],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 'organic',
|
||||
name: '有机质含量',
|
||||
value: 15 + Math.random() * 20,
|
||||
weight: factorWeights.find(f => f.id === 'organic')?.weight || 25,
|
||||
unit: 'g/kg',
|
||||
optimalRange: [20, 30],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 'depth',
|
||||
name: '土层厚度',
|
||||
value: 30 + Math.random() * 70,
|
||||
weight: factorWeights.find(f => f.id === 'depth')?.weight || 20,
|
||||
unit: 'cm',
|
||||
optimalRange: [50, 80],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 'nitrogen',
|
||||
name: '全氮',
|
||||
value: 0.8 + Math.random() * 1.5,
|
||||
weight: factorWeights.find(f => f.id === 'nitrogen')?.weight || 10,
|
||||
unit: 'g/kg',
|
||||
optimalRange: [1.0, 2.0],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 'phosphorus',
|
||||
name: '全磷',
|
||||
value: 0.4 + Math.random() * 1.2,
|
||||
weight: factorWeights.find(f => f.id === 'phosphorus')?.weight || 10,
|
||||
unit: 'g/kg',
|
||||
optimalRange: [0.6, 1.2],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 'potassium',
|
||||
name: '全钾',
|
||||
value: 12 + Math.random() * 13,
|
||||
weight: factorWeights.find(f => f.id === 'potassium')?.weight || 10,
|
||||
unit: 'g/kg',
|
||||
optimalRange: [15, 20],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 'drainage',
|
||||
name: '排水性',
|
||||
value: 60 + Math.random() * 40,
|
||||
weight: factorWeights.find(f => f.id === 'drainage')?.weight || 5,
|
||||
unit: '',
|
||||
optimalRange: [70, 90],
|
||||
score: 0
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 计算单个因子的得分
|
||||
const calculateFactorScore = (value: number, optimalRange: [number, number]): number => {
|
||||
const [min, max] = optimalRange;
|
||||
const mid = (min + max) / 2;
|
||||
const range = max - min;
|
||||
|
||||
if (value >= min && value <= max) {
|
||||
const deviation = Math.abs(value - mid);
|
||||
const deviationRatio = deviation / (range / 2);
|
||||
return Math.max(85, 100 - deviationRatio * 15);
|
||||
}
|
||||
|
||||
const deviation = value < min ? min - value : value - max;
|
||||
const deviationRatio = deviation / range;
|
||||
return Math.max(20, 85 - deviationRatio * 65);
|
||||
};
|
||||
|
||||
// 计算综合评分
|
||||
const calculateTotalScore = (factors: EvaluationFactor[]): number => {
|
||||
let totalScore = 0;
|
||||
for (const factor of factors) {
|
||||
totalScore += (factor.score * factor.weight) / 100;
|
||||
}
|
||||
return Math.round(totalScore);
|
||||
};
|
||||
|
||||
// 根据总分确定适宜性等级
|
||||
const getGrade = (totalScore: number): '高度适宜' | '一般适宜' | '不适宜' => {
|
||||
if (totalScore >= 80) return '高度适宜';
|
||||
if (totalScore >= 60) return '一般适宜';
|
||||
return '不适宜';
|
||||
};
|
||||
|
||||
const getGradeColor = (grade: string) => {
|
||||
switch (grade) {
|
||||
case '高度适宜': return 'bg-green-500';
|
||||
case '一般适宜': return 'bg-yellow-500';
|
||||
case '不适宜': return 'bg-red-500';
|
||||
default: return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 80) return 'text-green-600';
|
||||
if (score >= 60) return 'text-yellow-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
// 批量分析处理函数
|
||||
const handleRunBatchAnalysis = async () => {
|
||||
if (totalWeight !== 100) {
|
||||
toast.error('权重总和必须为100%才能进行批量分析');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBatchRunning(true);
|
||||
setBatchProgress(0);
|
||||
setBatchResults({
|
||||
total: 68,
|
||||
processed: 0,
|
||||
highSuitability: 0,
|
||||
mediumSuitability: 0,
|
||||
lowSuitability: 0,
|
||||
currentField: '',
|
||||
});
|
||||
setBatchAnalysisResults([]);
|
||||
|
||||
toast.success('开始批量分析,正在读取地块数据...');
|
||||
|
||||
const totalFields = 68;
|
||||
const results: SuitabilityResult[] = [];
|
||||
|
||||
for (let i = 0; i < totalFields; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const fieldId = `field-${i + 1}`;
|
||||
const fieldName = `地块${String.fromCharCode(65 + (i % 26))}${Math.floor(i / 26) + 1}`;
|
||||
|
||||
const factors = fetchFieldFactorsFromSpatialService(fieldId);
|
||||
const scoredFactors = factors.map(factor => ({
|
||||
...factor,
|
||||
score: calculateFactorScore(factor.value, factor.optimalRange)
|
||||
}));
|
||||
|
||||
const totalScore = calculateTotalScore(scoredFactors);
|
||||
const grade = getGrade(totalScore);
|
||||
|
||||
const result: SuitabilityResult = {
|
||||
fieldId,
|
||||
fieldName,
|
||||
totalScore,
|
||||
grade,
|
||||
factors: scoredFactors,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
results.push(result);
|
||||
|
||||
setBatchResults(prev => ({
|
||||
...prev,
|
||||
processed: i + 1,
|
||||
highSuitability: prev.highSuitability + (grade === '高度适宜' ? 1 : 0),
|
||||
mediumSuitability: prev.mediumSuitability + (grade === '一般适宜' ? 1 : 0),
|
||||
lowSuitability: prev.lowSuitability + (grade === '不适宜' ? 1 : 0),
|
||||
currentField: fieldName,
|
||||
}));
|
||||
|
||||
setBatchProgress(Math.round(((i + 1) / totalFields) * 100));
|
||||
}
|
||||
|
||||
setBatchAnalysisResults(results);
|
||||
setIsBatchRunning(false);
|
||||
|
||||
toast.success(`批量分析完成!已为${totalFields}个地块生成适宜性评价结果并更新到数据库`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user