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

299 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { 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%&ldquo;&rdquo;
</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>
);
}