子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View 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>: 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>
);
}