365 lines
16 KiB
TypeScript
365 lines
16 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
||
import { useRouter } from "next/router";
|
||
|
||
interface KnotPartsProps {
|
||
parts?: Array<{
|
||
detailid?: string;
|
||
codeonimage?: string | number;
|
||
oem?: string;
|
||
name?: string;
|
||
price?: string | number;
|
||
brand?: string;
|
||
availability?: string;
|
||
note?: string;
|
||
attributes?: Array<{ key: string; name?: string; value: string }>;
|
||
}>;
|
||
selectedCodeOnImage?: string | number;
|
||
catalogCode?: string;
|
||
vehicleId?: string;
|
||
highlightedCodeOnImage?: string | number | null; // Деталь подсвеченная при hover на изображении
|
||
selectedParts?: Set<string | number>; // Выбранные детали (множественный выбор)
|
||
onPartSelect?: (codeOnImage: string | number | null) => void; // Коллбек для выбора детали
|
||
onPartHover?: (codeOnImage: string | number | null) => void; // Коллбек для подсветки при hover
|
||
}
|
||
|
||
const KnotParts: React.FC<KnotPartsProps> = ({
|
||
parts = [],
|
||
selectedCodeOnImage,
|
||
catalogCode,
|
||
vehicleId,
|
||
highlightedCodeOnImage,
|
||
selectedParts = new Set(),
|
||
onPartSelect,
|
||
onPartHover
|
||
}) => {
|
||
const router = useRouter();
|
||
const [showTooltip, setShowTooltip] = useState(false);
|
||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
||
const [tooltipPart, setTooltipPart] = useState<any>(null);
|
||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||
|
||
// Отладочные логи для проверки данных
|
||
React.useEffect(() => {
|
||
console.log('🔍 KnotParts получил данные:', {
|
||
partsCount: parts.length,
|
||
firstPart: parts[0],
|
||
firstPartAttributes: parts[0]?.attributes?.length || 0,
|
||
allPartsWithAttributes: parts.map(part => ({
|
||
name: part.name,
|
||
oem: part.oem,
|
||
attributesCount: part.attributes?.length || 0,
|
||
attributes: part.attributes
|
||
}))
|
||
});
|
||
}, [parts]);
|
||
|
||
const handlePriceClick = (part: any) => {
|
||
if (part.oem && catalogCode && vehicleId !== undefined) {
|
||
// Переходим на страницу выбора бренда
|
||
const url = `/vehicle-search/${catalogCode}/${vehicleId}/part/${part.oem}/brands?detailName=${encodeURIComponent(part.name || '')}`;
|
||
router.push(url);
|
||
}
|
||
};
|
||
|
||
// Обработчик клика по детали в списке
|
||
const handlePartClick = (part: any) => {
|
||
if (part.codeonimage && onPartSelect) {
|
||
onPartSelect(part.codeonimage);
|
||
}
|
||
};
|
||
|
||
// Обработчики наведения
|
||
const handlePartMouseEnter = (part: any) => {
|
||
if (part.codeonimage && onPartHover) {
|
||
onPartHover(part.codeonimage);
|
||
}
|
||
};
|
||
|
||
const handlePartMouseLeave = () => {
|
||
if (onPartHover) {
|
||
onPartHover(null);
|
||
}
|
||
};
|
||
|
||
// Вычисляем позицию tooltip
|
||
const calculateTooltipPosition = (iconElement: HTMLElement) => {
|
||
if (!iconElement) {
|
||
console.error('❌ calculateTooltipPosition: элемент не найден');
|
||
return;
|
||
}
|
||
|
||
const rect = iconElement.getBoundingClientRect();
|
||
const tooltipWidth = 400;
|
||
const tooltipHeight = 300; // примерная высота
|
||
|
||
let x = rect.left + rect.width / 2 - tooltipWidth / 2;
|
||
let y = rect.bottom + 8;
|
||
|
||
// Проверяем, не выходит ли tooltip за границы экрана
|
||
if (x < 10) x = 10;
|
||
if (x + tooltipWidth > window.innerWidth - 10) {
|
||
x = window.innerWidth - tooltipWidth - 10;
|
||
}
|
||
|
||
// Если tooltip не помещается снизу, показываем сверху
|
||
if (y + tooltipHeight > window.innerHeight - 10) {
|
||
y = rect.top - tooltipHeight - 8;
|
||
}
|
||
|
||
setTooltipPosition({ x, y });
|
||
};
|
||
|
||
const handleInfoIconMouseEnter = (event: React.MouseEvent, part: any) => {
|
||
event.stopPropagation();
|
||
|
||
if (timeoutRef.current) {
|
||
clearTimeout(timeoutRef.current);
|
||
}
|
||
|
||
// Сохраняем ссылку на элемент до setTimeout
|
||
const target = event.currentTarget as HTMLElement;
|
||
|
||
timeoutRef.current = setTimeout(() => {
|
||
if (target && typeof target.getBoundingClientRect === 'function') {
|
||
calculateTooltipPosition(target);
|
||
setTooltipPart(part);
|
||
setShowTooltip(true);
|
||
console.log('🔍 Показываем тултип для детали:', part.name, 'Атрибуты:', part.attributes?.length || 0);
|
||
} else {
|
||
console.error('❌ handleInfoIconMouseEnter: элемент не поддерживает getBoundingClientRect:', target);
|
||
}
|
||
}, 300); // Задержка 300ms
|
||
};
|
||
|
||
const handleInfoIconMouseLeave = (event: React.MouseEvent) => {
|
||
event.stopPropagation();
|
||
|
||
if (timeoutRef.current) {
|
||
clearTimeout(timeoutRef.current);
|
||
}
|
||
|
||
timeoutRef.current = setTimeout(() => {
|
||
setShowTooltip(false);
|
||
setTooltipPart(null);
|
||
}, 100); // Небольшая задержка перед скрытием
|
||
};
|
||
|
||
// Очищаем таймеры при размонтировании
|
||
useEffect(() => {
|
||
return () => {
|
||
if (timeoutRef.current) {
|
||
clearTimeout(timeoutRef.current);
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// Если нет деталей, показываем заглушку
|
||
if (!parts || parts.length === 0) {
|
||
return (
|
||
<div className="knot-parts">
|
||
<div className="text-center py-8 text-gray-500">
|
||
<div className="text-lg font-medium mb-2">Список деталей</div>
|
||
<div className="text-sm">Выберите узел для отображения деталей</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Эффект для отслеживания изменений подсветки
|
||
useEffect(() => {
|
||
console.log('🔍 KnotParts - подсветка изменилась:', {
|
||
highlightedCodeOnImage,
|
||
highlightedType: typeof highlightedCodeOnImage,
|
||
partsCodeOnImages: parts.map(p => p.codeonimage),
|
||
partsDetailIds: parts.map(p => p.detailid),
|
||
willHighlight: parts.some(part =>
|
||
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage?.toString()) ||
|
||
(part.detailid && part.detailid.toString() === highlightedCodeOnImage?.toString())
|
||
),
|
||
willHighlightStrict: parts.some(part =>
|
||
part.codeonimage === highlightedCodeOnImage ||
|
||
part.detailid === highlightedCodeOnImage
|
||
),
|
||
firstPartWithCodeOnImage: parts.find(p => p.codeonimage)
|
||
});
|
||
|
||
// Детальная информация о всех деталях
|
||
console.log('📋 Все детали с их codeonimage и detailid:');
|
||
parts.forEach((part, idx) => {
|
||
console.log(` Деталь ${idx}: "${part.name}" codeonimage="${part.codeonimage}" (${typeof part.codeonimage}) detailid="${part.detailid}" (${typeof part.detailid})`);
|
||
});
|
||
|
||
console.log('🎯 Ищем подсветку для:', `"${highlightedCodeOnImage}" (${typeof highlightedCodeOnImage})`);
|
||
}, [highlightedCodeOnImage, parts]);
|
||
|
||
return (
|
||
<>
|
||
{/* Статус выбранных деталей */}
|
||
{selectedParts.size > 0 && (
|
||
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mb-4">
|
||
<div className="flex items-center">
|
||
<svg className="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<span className="text-green-800 font-medium">
|
||
Выбрано деталей: {selectedParts.size}
|
||
</span>
|
||
<span className="text-green-600 text-sm ml-2">
|
||
(Кликните по детали, чтобы убрать из выбранных)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="knot-parts">
|
||
{parts.map((part, idx) => {
|
||
const isHighlighted = highlightedCodeOnImage !== null && highlightedCodeOnImage !== undefined && (
|
||
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage.toString()) ||
|
||
(part.detailid && part.detailid.toString() === highlightedCodeOnImage.toString())
|
||
);
|
||
|
||
const isSelected = selectedParts.has(part.detailid || part.codeonimage || idx.toString());
|
||
|
||
// Создаем уникальный ключ
|
||
const uniqueKey = `part-${idx}-${part.detailid || part.oem || part.name || 'unknown'}`;
|
||
|
||
return (
|
||
<div
|
||
key={uniqueKey}
|
||
className={`part-item p-4 border rounded-lg cursor-pointer transition-colors ${
|
||
isSelected
|
||
? 'bg-green-100 border-green-500'
|
||
: isHighlighted
|
||
? 'bg-yellow-100 border-yellow-500'
|
||
: 'bg-white border-gray-200 hover:border-gray-300'
|
||
}`}
|
||
onClick={() => handlePartClick(part)}
|
||
onMouseEnter={() => handlePartMouseEnter(part)}
|
||
onMouseLeave={handlePartMouseLeave}
|
||
style={{ cursor: 'pointer' }}
|
||
>
|
||
<div className="w-layout-hflex flex-block-116">
|
||
<div
|
||
className={`nuberlist ${isSelected ? 'text-green-700 font-bold' : isHighlighted ? 'text-red-700 font-bold' : ''}`}
|
||
>
|
||
{part.codeonimage || idx + 1}
|
||
</div>
|
||
<div className="oemnuber">{part.oem}</div>
|
||
</div>
|
||
<div className={`partsname ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? 'text-red-800 font-semibold' : ''}`}>
|
||
{part.name}
|
||
</div>
|
||
<div className="w-layout-hflex flex-block-117">
|
||
<button
|
||
className="button-3 w-button"
|
||
onClick={(e) => {
|
||
e.stopPropagation(); // Предотвращаем срабатывание onClick родительского элемента
|
||
handlePriceClick(part);
|
||
}}
|
||
style={{ cursor: 'pointer' }}
|
||
>
|
||
Цена
|
||
</button>
|
||
<div
|
||
className="code-embed-16 w-embed cursor-pointer hover:opacity-70 transition-opacity"
|
||
onMouseEnter={(e) => handleInfoIconMouseEnter(e, part)}
|
||
onMouseLeave={handleInfoIconMouseLeave}
|
||
style={{ cursor: 'pointer' }}
|
||
>
|
||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M8.1 13.5H9.89999V8.1H8.1V13.5ZM8.99999 6.3C9.25499 6.3 9.46889 6.2136 9.64169 6.0408C9.81449 5.868 9.90059 5.6544 9.89999 5.4C9.89939 5.1456 9.81299 4.932 9.64079 4.7592C9.46859 4.5864 9.25499 4.5 8.99999 4.5C8.745 4.5 8.53139 4.5864 8.35919 4.7592C8.187 4.932 8.1006 5.1456 8.1 5.4C8.0994 5.6544 8.1858 5.8683 8.35919 6.0417C8.53259 6.2151 8.74619 6.3012 8.99999 6.3ZM8.99999 18C7.755 18 6.585 17.7636 5.49 17.2908C4.395 16.818 3.4425 16.1769 2.6325 15.3675C1.8225 14.5581 1.1814 13.6056 0.709201 12.51C0.237001 11.4144 0.000601139 10.2444 1.13924e-06 9C-0.00059886 7.7556 0.235801 6.5856 0.709201 5.49C1.1826 4.3944 1.8237 3.4419 2.6325 2.6325C3.4413 1.8231 4.3938 1.182 5.49 0.7092C6.5862 0.2364 7.7562 0 8.99999 0C10.2438 0 11.4138 0.2364 12.51 0.7092C13.6062 1.182 14.5587 1.8231 15.3675 2.6325C16.1763 3.4419 16.8177 4.3944 17.2917 5.49C17.7657 6.5856 18.0018 7.7556 18 9C17.9982 10.2444 17.7618 11.4144 17.2908 12.51C16.8198 13.6056 16.1787 14.5581 15.3675 15.3675C14.5563 16.1769 13.6038 16.8183 12.51 17.2917C11.4162 17.7651 10.2462 18.0012 8.99999 18Z" fill="currentcolor" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Красивый тултип с информацией о детали */}
|
||
{showTooltip && tooltipPart && (
|
||
<div
|
||
className="tooltip-detail-modern"
|
||
style={{
|
||
position: 'fixed',
|
||
left: tooltipPosition.x,
|
||
top: tooltipPosition.y,
|
||
zIndex: 1000,
|
||
pointerEvents: 'none'
|
||
}}
|
||
>
|
||
<div className="tooltip-content-modern">
|
||
{/* Стрелка тултипа */}
|
||
<div className="tooltip-arrow"></div>
|
||
|
||
{/* Заголовок */}
|
||
<div className="tooltip-header-modern">
|
||
<div className="tooltip-icon">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M12 2L13.09 8.26L20 9L13.09 9.74L12 16L10.91 9.74L4 9L10.91 8.26L12 2Z" fill="currentColor"/>
|
||
</svg>
|
||
</div>
|
||
<div className="tooltip-title-section">
|
||
<h4 className="tooltip-title">{tooltipPart.name}</h4>
|
||
{tooltipPart.oem && (
|
||
<div className="tooltip-oem-badge">
|
||
<span className="tooltip-oem-label">OEM</span>
|
||
<span className="tooltip-oem-value">{tooltipPart.oem}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Контент */}
|
||
<div className="tooltip-body-modern">
|
||
{tooltipPart.attributes && tooltipPart.attributes.length > 0 ? (
|
||
<div className="tooltip-attributes-modern">
|
||
<div className="tooltip-section-title">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
Характеристики
|
||
</div>
|
||
<div className="tooltip-attributes-grid">
|
||
{tooltipPart.attributes.map((attr: any, index: number) => (
|
||
<div key={index} className="tooltip-attribute-item">
|
||
<div className="tooltip-attribute-key">{attr.name || attr.key}</div>
|
||
<div className="tooltip-attribute-value">{attr.value}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="tooltip-no-data">
|
||
<div className="tooltip-no-data-icon">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M12 9V13M12 17H12.01M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
<div className="tooltip-no-data-text">
|
||
<div>Дополнительная информация</div>
|
||
<div>недоступна</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{tooltipPart.note && (
|
||
<div className="tooltip-note-modern">
|
||
<div className="tooltip-section-title">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M11 7H13V9H11V7ZM11 11H13V17H11V11ZM12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2Z" fill="currentColor"/>
|
||
</svg>
|
||
Примечание
|
||
</div>
|
||
<div className="tooltip-note-text">{tooltipPart.note}</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default KnotParts;
|