299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
'use client';
|
||
|
||
import { Card } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Slider } from '@/components/ui/slider';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Progress } from '@/components/ui/progress';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog';
|
||
import {
|
||
Sliders,
|
||
RotateCcw,
|
||
Info,
|
||
Scale
|
||
} from 'lucide-react';
|
||
import { SpatialAnalysisState, SpatialAnalysisAction } from './spatialAnalysisReducer';
|
||
|
||
interface WeightConfigurationProps {
|
||
state: SpatialAnalysisState;
|
||
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||
isDialog?: boolean;
|
||
}
|
||
|
||
export default function WeightConfiguration({ state, dispatch, isDialog = false }: WeightConfigurationProps) {
|
||
const categories = [
|
||
{ id: 'soil', name: '土壤条件', icon: '🌱', color: 'blue' },
|
||
{ id: 'climate', name: '气候条件', icon: '🌤️', color: 'green' },
|
||
{ id: 'topography', name: '地形条件', icon: '⛰️', color: 'orange' },
|
||
{ id: 'infrastructure', name: '基础设施', icon: '🏗️', color: 'purple' }
|
||
];
|
||
|
||
const handleUpdateWeight = (category: string, value: number) => {
|
||
const newWeightConfig = {
|
||
...state.weightConfig,
|
||
[category]: value / 100
|
||
};
|
||
dispatch({ type: 'SET_WEIGHT_CONFIG', payload: newWeightConfig });
|
||
};
|
||
|
||
const handleResetToDefaults = () => {
|
||
const defaultConfig = {
|
||
soil: 0.35,
|
||
climate: 0.30,
|
||
topography: 0.20,
|
||
infrastructure: 0.15
|
||
};
|
||
dispatch({ type: 'SET_WEIGHT_CONFIG', payload: defaultConfig });
|
||
};
|
||
|
||
const handleBalanceWeights = () => {
|
||
// 自动平衡权重,确保总和为100%
|
||
const total = Object.values(state.weightConfig).reduce((sum, val) => sum + val, 0);
|
||
if (total > 0) {
|
||
const balancedConfig = Object.fromEntries(
|
||
Object.entries(state.weightConfig).map(([key, value]) => [key, value / total])
|
||
) as typeof state.weightConfig;
|
||
dispatch({ type: 'SET_WEIGHT_CONFIG', payload: balancedConfig });
|
||
}
|
||
};
|
||
|
||
const getTotalWeight = () => {
|
||
return Object.values(state.weightConfig).reduce((sum, val) => sum + val, 0);
|
||
};
|
||
|
||
const categoryConfigs = categories.map(category => ({
|
||
...category,
|
||
weight: state.weightConfig[category.id as keyof typeof state.weightConfig],
|
||
percentage: Math.round(state.weightConfig[category.id as keyof typeof state.weightConfig] * 100)
|
||
}));
|
||
|
||
const content = (
|
||
<div className="space-y-6">
|
||
{/* 权重总览 */}
|
||
<Card className="p-6">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-medium flex items-center gap-2">
|
||
<Scale className="w-5 h-5" />
|
||
权重分配总览
|
||
</h3>
|
||
<div className="flex items-center gap-3">
|
||
<div className="text-right">
|
||
<div className={`text-2xl font-bold ${
|
||
Math.abs(getTotalWeight() - 1) < 0.01 ? 'text-green-600' : 'text-red-600'
|
||
}`}>
|
||
{Math.round(getTotalWeight() * 100)}%
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">总权重</div>
|
||
</div>
|
||
<Button variant="outline" size="sm" onClick={handleResetToDefaults}>
|
||
<RotateCcw className="w-4 h-4 mr-2" />
|
||
重置默认
|
||
</Button>
|
||
<Button variant="outline" size="sm" onClick={handleBalanceWeights}>
|
||
<Sliders className="w-4 h-4 mr-2" />
|
||
自动平衡
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 权重可视化 */}
|
||
<div className="space-y-4">
|
||
{categoryConfigs.map(category => (
|
||
<div key={category.id} className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-lg">{category.icon}</span>
|
||
<span className="font-medium">{category.name}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-lg font-bold">{category.percentage}%</span>
|
||
<Badge
|
||
variant="outline"
|
||
className={
|
||
category.id === 'soil' ? 'border-blue-200 text-blue-600' :
|
||
category.id === 'climate' ? 'border-green-200 text-green-600' :
|
||
category.id === 'topography' ? 'border-orange-200 text-orange-600' :
|
||
'border-purple-200 text-purple-600'
|
||
}
|
||
>
|
||
{category.weight.toFixed(2)}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
|
||
<Progress
|
||
value={category.weight * 100}
|
||
className={`h-3 ${
|
||
category.id === 'soil' ? 'bg-blue-100' :
|
||
category.id === 'climate' ? 'bg-green-100' :
|
||
category.id === 'topography' ? 'bg-orange-100' :
|
||
'bg-purple-100'
|
||
}`}
|
||
/>
|
||
</div>
|
||
))}
|
||
|
||
{Math.abs(getTotalWeight() - 1) > 0.01 && (
|
||
<div className="pt-2 border-t">
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-muted-foreground">偏差</span>
|
||
<span className="text-red-600 font-medium">
|
||
{Math.abs((getTotalWeight() - 1) * 100).toFixed(1)}%
|
||
</span>
|
||
</div>
|
||
<Progress value={getTotalWeight() * 100} className="h-2 mt-1" />
|
||
<p className="text-xs text-red-600 mt-1">
|
||
权重总和不为100%,建议点击“自动平衡”调整
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 权重调节面板 */}
|
||
<Card className="p-6">
|
||
<h3 className="text-lg font-medium mb-6">权重调节</h3>
|
||
|
||
<div className="space-y-6">
|
||
{categoryConfigs.map(category => (
|
||
<div key={category.id} className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xl">{category.icon}</span>
|
||
<div>
|
||
<h4 className="font-medium">{category.name}</h4>
|
||
<p className="text-sm text-muted-foreground">
|
||
{category.id === 'soil' && '包括土壤pH、有机质、养分含量等指标'}
|
||
{category.id === 'climate' && '包括温度、降水、光照等气象条件'}
|
||
{category.id === 'topography' && '包括海拔、坡度等地形特征'}
|
||
{category.id === 'infrastructure' && '包括灌溉、交通等设施条件'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="text-right min-w-[80px]">
|
||
<div className="text-2xl font-bold">{category.percentage}%</div>
|
||
<div className="text-xs text-muted-foreground">当前权重</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Slider
|
||
value={[category.weight * 100]}
|
||
onValueChange={([value]) => handleUpdateWeight(category.id, value)}
|
||
max={60}
|
||
min={5}
|
||
step={1}
|
||
className="w-full"
|
||
/>
|
||
<div className="flex justify-between text-xs text-muted-foreground">
|
||
<span>5%</span>
|
||
<span>60%</span>
|
||
</div>
|
||
</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 gap-2">
|
||
<Info className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||
<p className="font-medium mb-2">权重配置说明:</p>
|
||
<ul className="space-y-1 text-xs">
|
||
<li>• <strong>土壤条件 (35%)</strong>: 土壤是农业生产的基础,影响作物生长和养分供应</li>
|
||
<li>• <strong>气候条件 (30%)</strong>: 气候决定了作物的生长环境和适宜种植范围</li>
|
||
<li>• <strong>地形条件 (20%)</strong>: 地形影响水土保持、机械化作业和微气候</li>
|
||
<li>• <strong>基础设施 (15%)</strong>: 灌溉、交通等设施影响生产效率和成本</li>
|
||
<li>• 权重总和应保持100%,可根据实际情况调整各维度的重要性</li>
|
||
<li>• 建议在农业专家指导下进行权重配置,确保评价结果科学合理</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 预设配置 */}
|
||
<Card className="p-4">
|
||
<h3 className="font-medium mb-4">快速预设配置</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => dispatch({
|
||
type: 'SET_WEIGHT_CONFIG',
|
||
payload: { soil: 0.4, climate: 0.3, topography: 0.2, infrastructure: 0.1 }
|
||
})}
|
||
>
|
||
<div className="text-left">
|
||
<div className="font-medium text-sm">粮食作物优先</div>
|
||
<div className="text-xs text-muted-foreground">土壤40% · 气候30%</div>
|
||
</div>
|
||
</Button>
|
||
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => dispatch({
|
||
type: 'SET_WEIGHT_CONFIG',
|
||
payload: { soil: 0.3, climate: 0.4, topography: 0.2, infrastructure: 0.1 }
|
||
})}
|
||
>
|
||
<div className="text-left">
|
||
<div className="font-medium text-sm">经济作物优先</div>
|
||
<div className="text-xs text-muted-foreground">气候40% · 土壤30%</div>
|
||
</div>
|
||
</Button>
|
||
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => dispatch({
|
||
type: 'SET_WEIGHT_CONFIG',
|
||
payload: { soil: 0.3, climate: 0.3, topography: 0.3, infrastructure: 0.1 }
|
||
})}
|
||
>
|
||
<div className="text-left">
|
||
<div className="font-medium text-sm">均衡发展</div>
|
||
<div className="text-xs text-muted-foreground">三要素各30%</div>
|
||
</div>
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
);
|
||
|
||
if (isDialog) {
|
||
return (
|
||
<Dialog open={state.showWeightDialog} onOpenChange={() => dispatch({ type: 'TOGGLE_WEIGHT_DIALOG' })}>
|
||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
<Sliders className="w-5 h-5" />
|
||
权重配置
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
{content}
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-medium">权重配置</h3>
|
||
<div className="flex items-center gap-2">
|
||
<Badge variant={Math.abs(getTotalWeight() - 1) < 0.01 ? 'default' : 'destructive'}>
|
||
总权重: {Math.round(getTotalWeight() * 100)}%
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
|
||
{content}
|
||
</div>
|
||
);
|
||
} |