1234 #23
@ -28,6 +28,7 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
const [imageLoadTimeout, setImageLoadTimeout] = useState<NodeJS.Timeout | null>(null);
|
const [imageLoadTimeout, setImageLoadTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||||
const [selectedDetail, setSelectedDetail] = useState<LaximoUnitDetail | null>(null);
|
const [selectedDetail, setSelectedDetail] = useState<LaximoUnitDetail | null>(null);
|
||||||
|
const [highlightedDetailId, setHighlightedDetailId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Отладочная информация для SSD
|
// Отладочная информация для SSD
|
||||||
console.log('🔍 UnitDetailsSection получил SSD:', {
|
console.log('🔍 UnitDetailsSection получил SSD:', {
|
||||||
@ -165,11 +166,31 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
d.detailid === coord.codeonimage
|
d.detailid === coord.codeonimage
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (detail) {
|
||||||
|
console.log('✅ Найдена деталь для выделения:', detail.name, 'ID:', detail.detailid);
|
||||||
|
// Выделяем деталь в списке
|
||||||
|
setHighlightedDetailId(detail.detailid);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Деталь не найдена в списке по коду:', coord.codeonimage);
|
||||||
|
setHighlightedDetailId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCoordinateDoubleClick = (coord: LaximoImageCoordinate) => {
|
||||||
|
console.log('🖱️ Двойной клик по интерактивной области:', coord.codeonimage);
|
||||||
|
|
||||||
|
// Сначала пытаемся найти деталь в списке
|
||||||
|
const detail = unitDetails.find(d =>
|
||||||
|
d.detailid === coord.detailid ||
|
||||||
|
d.codeonimage === coord.codeonimage ||
|
||||||
|
d.detailid === coord.codeonimage
|
||||||
|
);
|
||||||
|
|
||||||
if (detail && detail.oem) {
|
if (detail && detail.oem) {
|
||||||
console.log('✅ Найдена деталь для выбора бренда:', detail.name, 'OEM:', detail.oem);
|
console.log('✅ Найдена деталь для выбора бренда:', detail.name, 'OEM:', detail.oem);
|
||||||
// Показываем модал выбора бренда
|
// Переходим на страницу выбора бренда
|
||||||
setSelectedDetail(detail);
|
const url = `/vehicle-search/${catalogCode}/${vehicleId}/part/${detail.oem}/brands?detailName=${encodeURIComponent(detail.name || '')}`;
|
||||||
setIsBrandModalOpen(true);
|
router.push(url);
|
||||||
} else {
|
} else {
|
||||||
// Если деталь не найдена в списке, переходим к общему поиску по коду на изображении
|
// Если деталь не найдена в списке, переходим к общему поиску по коду на изображении
|
||||||
console.log('⚠️ Деталь не найдена в списке, переходим к поиску по коду:', coord.codeonimage);
|
console.log('⚠️ Деталь не найдена в списке, переходим к поиску по коду:', coord.codeonimage);
|
||||||
@ -461,7 +482,8 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
borderRadius: coord.shape === 'circle' ? '50%' : '0'
|
borderRadius: coord.shape === 'circle' ? '50%' : '0'
|
||||||
}}
|
}}
|
||||||
onClick={() => handleCoordinateClick(coord)}
|
onClick={() => handleCoordinateClick(coord)}
|
||||||
title={detail ? `${coord.codeonimage}: ${detail.name}` : `Деталь ${coord.codeonimage}`}
|
onDoubleClick={() => handleCoordinateDoubleClick(coord)}
|
||||||
|
title={detail ? `${coord.codeonimage}: ${detail.name} (Клик - выделить, двойной клик - перейти к выбору бренда)` : `Деталь ${coord.codeonimage} (Клик - выделить, двойной клик - поиск)`}
|
||||||
>
|
>
|
||||||
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 bg-red-600 text-white text-xs px-2 py-1 rounded font-bold">
|
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 bg-red-600 text-white text-xs px-2 py-1 rounded font-bold">
|
||||||
{coord.codeonimage}
|
{coord.codeonimage}
|
||||||
@ -612,7 +634,11 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
{unitDetails.map((detail, index) => (
|
{unitDetails.map((detail, index) => (
|
||||||
<div
|
<div
|
||||||
key={`detail-${unitId}-${index}-${detail.detailid}`}
|
key={`detail-${unitId}-${index}-${detail.detailid}`}
|
||||||
className="border border-gray-200 rounded-lg p-4 hover:border-red-300 hover:shadow-md transition-all duration-200 cursor-pointer"
|
className={`border rounded-lg p-4 hover:border-red-300 hover:shadow-md transition-all duration-200 cursor-pointer ${
|
||||||
|
highlightedDetailId === detail.detailid
|
||||||
|
? 'border-red-500 bg-red-50 shadow-md'
|
||||||
|
: 'border-gray-200'
|
||||||
|
}`}
|
||||||
onClick={() => handleDetailClick(detail)}
|
onClick={() => handleDetailClick(detail)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
|
@ -21,6 +21,9 @@ interface KnotInProps {
|
|||||||
note?: string;
|
note?: string;
|
||||||
attributes?: Array<{ key: string; name?: string; value: string }>;
|
attributes?: Array<{ key: string; name?: string; value: string }>;
|
||||||
}>;
|
}>;
|
||||||
|
onPartSelect?: (codeOnImage: string | number | null) => void; // Коллбек для уведомления KnotParts о выделении детали
|
||||||
|
onPartsHighlight?: (codeOnImage: string | number | null) => void; // Коллбек для подсветки при hover
|
||||||
|
selectedParts?: Set<string | number>; // Выбранные детали (множественный выбор)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для корректного формирования URL изображения
|
// Функция для корректного формирования URL изображения
|
||||||
@ -34,12 +37,23 @@ const getImageUrl = (baseUrl: string, size: string) => {
|
|||||||
.replace('%size%', size);
|
.replace('%size%', size);
|
||||||
};
|
};
|
||||||
|
|
||||||
const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, unitName, parts }) => {
|
const KnotIn: React.FC<KnotInProps> = ({
|
||||||
|
catalogCode,
|
||||||
|
vehicleId,
|
||||||
|
ssd,
|
||||||
|
unitId,
|
||||||
|
unitName,
|
||||||
|
parts,
|
||||||
|
onPartSelect,
|
||||||
|
onPartsHighlight,
|
||||||
|
selectedParts = new Set()
|
||||||
|
}) => {
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
||||||
const selectedImageSize = 'source';
|
const selectedImageSize = 'source';
|
||||||
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||||
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
|
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
|
||||||
|
const [hoveredCodeOnImage, setHoveredCodeOnImage] = useState<string | number | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Получаем инфо об узле (для картинки)
|
// Получаем инфо об узле (для картинки)
|
||||||
@ -150,21 +164,62 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Клик по точке: найти part по codeonimage/detailid и открыть BrandSelectionModal
|
// Обработчик наведения на точку
|
||||||
const handlePointClick = (codeonimage: string | number) => {
|
const handlePointHover = (coord: any) => {
|
||||||
|
// Попробуем использовать разные поля для связи
|
||||||
|
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||||
|
|
||||||
|
console.log('🔍 KnotIn - hover на точку:', {
|
||||||
|
coord,
|
||||||
|
detailid: coord.detailid,
|
||||||
|
codeonimage: coord.codeonimage,
|
||||||
|
code: coord.code,
|
||||||
|
identifierToUse,
|
||||||
|
type: typeof identifierToUse,
|
||||||
|
coordinatesLength: coordinates.length,
|
||||||
|
partsLength: parts?.length || 0,
|
||||||
|
firstCoord: coordinates[0],
|
||||||
|
firstPart: parts?.[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
setHoveredCodeOnImage(identifierToUse);
|
||||||
|
if (onPartsHighlight) {
|
||||||
|
onPartsHighlight(identifierToUse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Клик по точке: выделить в списке деталей
|
||||||
|
const handlePointClick = (coord: any) => {
|
||||||
if (!parts) return;
|
if (!parts) return;
|
||||||
console.log('Клик по точке:', codeonimage, 'Все детали:', parts);
|
|
||||||
|
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||||
|
console.log('Клик по точке:', identifierToUse, 'Координата:', coord, 'Все детали:', parts);
|
||||||
|
|
||||||
|
// Уведомляем родительский компонент о выборе детали для выделения в списке
|
||||||
|
if (onPartSelect) {
|
||||||
|
onPartSelect(identifierToUse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Двойной клик по точке: переход на страницу выбора бренда
|
||||||
|
const handlePointDoubleClick = (coord: any) => {
|
||||||
|
if (!parts) return;
|
||||||
|
|
||||||
|
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||||
|
console.log('Двойной клик по точке:', identifierToUse, 'Координата:', coord);
|
||||||
|
|
||||||
const part = parts.find(
|
const part = parts.find(
|
||||||
(p) =>
|
(p) =>
|
||||||
(p.codeonimage && p.codeonimage.toString() === codeonimage.toString()) ||
|
(p.detailid && p.detailid.toString() === identifierToUse?.toString()) ||
|
||||||
(p.detailid && p.detailid.toString() === codeonimage.toString())
|
(p.codeonimage && p.codeonimage.toString() === identifierToUse?.toString())
|
||||||
);
|
);
|
||||||
console.log('Найдена деталь для точки:', part);
|
|
||||||
if (part?.oem) {
|
if (part?.oem) {
|
||||||
setSelectedDetail({ oem: part.oem, name: part.name || '' });
|
// Переходим на страницу выбора бренда вместо модального окна
|
||||||
setIsBrandModalOpen(true);
|
const url = `/vehicle-search/${catalogCode}/${vehicleId}/part/${part.oem}/brands?detailName=${encodeURIComponent(part.name || '')}`;
|
||||||
|
router.push(url);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Нет артикула (oem) для выбранной точки:', codeonimage, part);
|
console.warn('Нет артикула (oem) для выбранной точки:', identifierToUse, part);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,6 +227,40 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('KnotIn parts:', parts);
|
console.log('KnotIn parts:', parts);
|
||||||
console.log('KnotIn coordinates:', coordinates);
|
console.log('KnotIn coordinates:', coordinates);
|
||||||
|
if (coordinates.length > 0) {
|
||||||
|
console.log('🔍 Первые 5 координат:', coordinates.slice(0, 5).map((c: any) => ({
|
||||||
|
code: c.code,
|
||||||
|
codeonimage: c.codeonimage,
|
||||||
|
detailid: c.detailid,
|
||||||
|
x: c.x,
|
||||||
|
y: c.y
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
if (parts && parts.length > 0) {
|
||||||
|
console.log('🔍 Первые 5 деталей:', parts.slice(0, 5).map(p => ({
|
||||||
|
name: p.name,
|
||||||
|
codeonimage: p.codeonimage,
|
||||||
|
detailid: p.detailid,
|
||||||
|
oem: p.oem
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попытка связать координаты с деталями
|
||||||
|
if (coordinates.length > 0 && parts && parts.length > 0) {
|
||||||
|
console.log('🔗 Попытка связать координаты с деталями:');
|
||||||
|
coordinates.forEach((coord: any, idx: number) => {
|
||||||
|
const matchingPart = parts.find(part =>
|
||||||
|
part.detailid === coord.detailid ||
|
||||||
|
part.codeonimage === coord.codeonimage ||
|
||||||
|
part.codeonimage === coord.code
|
||||||
|
);
|
||||||
|
if (matchingPart) {
|
||||||
|
console.log(` ✅ Координата ${idx}: detailid=${coord.detailid}, codeonimage=${coord.codeonimage} -> Деталь: ${matchingPart.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ Координата ${idx}: detailid=${coord.detailid}, codeonimage=${coord.codeonimage} -> НЕ НАЙДЕНА`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [parts, coordinates]);
|
}, [parts, coordinates]);
|
||||||
|
|
||||||
if (unitInfoLoading || imageMapLoading) {
|
if (unitInfoLoading || imageMapLoading) {
|
||||||
@ -222,11 +311,16 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<<<<<<< HEAD
|
||||||
<div className="relative inline-block p-5" style={{ borderRadius: 8, background: '#fff' }}>
|
<div className="relative inline-block p-5" style={{ borderRadius: 8, background: '#fff' }}>
|
||||||
{/* ВРЕМЕННО: выводим количество точек для быстрой проверки */}
|
{/* ВРЕМЕННО: выводим количество точек для быстрой проверки */}
|
||||||
{/* <div style={{ position: 'absolute', top: 4, left: 4, zIndex: 20, background: 'rgba(255,0,0,0.1)', color: '#c00', fontWeight: 700, fontSize: 14, padding: '2px 8px', borderRadius: 6 }}>
|
{/* <div style={{ position: 'absolute', top: 4, left: 4, zIndex: 20, background: 'rgba(255,0,0,0.1)', color: '#c00', fontWeight: 700, fontSize: 14, padding: '2px 8px', borderRadius: 6 }}>
|
||||||
{coordinates.length} точек
|
{coordinates.length} точек
|
||||||
</div> */}
|
</div> */}
|
||||||
|
=======
|
||||||
|
<div className="relative inline-block">
|
||||||
|
|
||||||
|
>>>>>>> c1e0d46 (Добавлены новые функции для выделения и выбора деталей в компонентах UnitDetailsSection и KnotIn. Реализованы обработчики кликов и наведения, а также улучшено взаимодействие с пользователем через подсветку выбранных деталей. Обновлены компоненты KnotParts и VehicleDetailsPage для поддержки множественного выбора деталей и логирования данных от API.)
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
@ -242,38 +336,63 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
const size = 22;
|
const size = 22;
|
||||||
const scaledX = coord.x * imageScale.x - size / 2;
|
const scaledX = coord.x * imageScale.x - size / 2;
|
||||||
const scaledY = coord.y * imageScale.y - size / 2;
|
const scaledY = coord.y * imageScale.y - size / 2;
|
||||||
|
|
||||||
|
// Используем code или codeonimage в зависимости от структуры данных
|
||||||
|
const codeValue = coord.code || coord.codeonimage;
|
||||||
|
|
||||||
|
// Определяем состояние точки
|
||||||
|
const isSelected = selectedParts.has(codeValue);
|
||||||
|
const isHovered = hoveredCodeOnImage === codeValue;
|
||||||
|
|
||||||
|
// Определяем цвета на основе состояния
|
||||||
|
let backgroundColor = '#B7CAE2'; // Базовый цвет
|
||||||
|
let textColor = '#000';
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
backgroundColor = '#22C55E'; // Зеленый для выбранных
|
||||||
|
textColor = '#fff';
|
||||||
|
} else if (isHovered) {
|
||||||
|
backgroundColor = '#EC1C24'; // Красный при наведении
|
||||||
|
textColor = '#fff';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`Деталь ${coord.codeonimage}`}
|
aria-label={`Деталь ${codeValue}`}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
|
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord);
|
||||||
}}
|
}}
|
||||||
className="absolute flex items-center justify-center cursor-pointer transition-colors"
|
className="absolute flex items-center justify-center cursor-pointer transition-all duration-200 ease-in-out"
|
||||||
style={{
|
style={{
|
||||||
left: scaledX,
|
left: scaledX,
|
||||||
top: scaledY,
|
top: scaledY,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
background: '#B7CAE2',
|
backgroundColor,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
|
border: isSelected ? '2px solid #16A34A' : 'none',
|
||||||
|
transform: isHovered || isSelected ? 'scale(1.1)' : 'scale(1)',
|
||||||
|
zIndex: isHovered || isSelected ? 10 : 1,
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
}}
|
}}
|
||||||
title={coord.codeonimage}
|
title={`${codeValue} (Клик - выделить в списке, двойной клик - перейти к выбору бренда)`}
|
||||||
onClick={() => handlePointClick(coord.codeonimage)}
|
onClick={() => handlePointClick(coord)}
|
||||||
onMouseEnter={e => {
|
onDoubleClick={() => handlePointDoubleClick(coord)}
|
||||||
(e.currentTarget as HTMLDivElement).style.background = '#EC1C24';
|
onMouseEnter={() => handlePointHover(coord)}
|
||||||
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#fff';
|
onMouseLeave={() => {
|
||||||
}}
|
setHoveredCodeOnImage(null);
|
||||||
onMouseLeave={e => {
|
if (onPartsHighlight) {
|
||||||
(e.currentTarget as HTMLDivElement).style.background = '#B7CAE2';
|
onPartsHighlight(null);
|
||||||
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#000';
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none" style={{color: '#000'}}>
|
<span
|
||||||
{coord.codeonimage}
|
className="flex items-center justify-center w-full h-full text-sm font-bold select-none pointer-events-none transition-colors duration-200"
|
||||||
|
style={{ color: textColor }}
|
||||||
|
>
|
||||||
|
{codeValue}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface KnotPartsProps {
|
interface KnotPartsProps {
|
||||||
@ -16,10 +16,42 @@ interface KnotPartsProps {
|
|||||||
selectedCodeOnImage?: string | number;
|
selectedCodeOnImage?: string | number;
|
||||||
catalogCode?: string;
|
catalogCode?: string;
|
||||||
vehicleId?: 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 }) => {
|
const KnotParts: React.FC<KnotPartsProps> = ({
|
||||||
|
parts = [],
|
||||||
|
selectedCodeOnImage,
|
||||||
|
catalogCode,
|
||||||
|
vehicleId,
|
||||||
|
highlightedCodeOnImage,
|
||||||
|
selectedParts = new Set(),
|
||||||
|
onPartSelect,
|
||||||
|
onPartHover
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
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) => {
|
const handlePriceClick = (part: any) => {
|
||||||
if (part.oem && catalogCode && vehicleId !== undefined) {
|
if (part.oem && catalogCode && vehicleId !== undefined) {
|
||||||
@ -29,6 +61,98 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчик клика по детали в списке
|
||||||
|
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) {
|
if (!parts || parts.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -41,29 +165,107 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Эффект для отслеживания изменений подсветки
|
||||||
|
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 (
|
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">
|
<div className="knot-parts">
|
||||||
{parts.map((part, idx) => {
|
{parts.map((part, idx) => {
|
||||||
const isSelected = part.codeonimage && part.codeonimage === selectedCodeOnImage;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-layout-hflex knotlistitem border rounded transition-colors duration-150 ${isSelected ? 'bg-yellow-100 border-yellow-400' : 'border-transparent'}`}
|
key={uniqueKey}
|
||||||
key={part.detailid || idx}
|
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="w-layout-hflex flex-block-116">
|
||||||
<div className="nuberlist">{part.codeonimage || idx + 1}</div>
|
<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 className="oemnuber">{part.oem}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="partsname">{part.name}</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">
|
<div className="w-layout-hflex flex-block-117">
|
||||||
<button
|
<button
|
||||||
className="button-3 w-button"
|
className="button-3 w-button"
|
||||||
onClick={() => handlePriceClick(part)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // Предотвращаем срабатывание onClick родительского элемента
|
||||||
|
handlePriceClick(part);
|
||||||
|
}}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
Цена
|
Цена
|
||||||
</button>
|
</button>
|
||||||
<div className="code-embed-16 w-embed">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
@ -73,6 +275,89 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -54,26 +54,40 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по цене
|
// Получаем все доступные предложения для расчета диапазонов
|
||||||
const prices: number[] = [];
|
const allAvailableOffers: any[] = [];
|
||||||
|
|
||||||
|
// Добавляем основные предложения
|
||||||
result.internalOffers?.forEach((offer: any) => {
|
result.internalOffers?.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push(offer);
|
||||||
});
|
});
|
||||||
result.externalOffers?.forEach((offer: any) => {
|
result.externalOffers?.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push(offer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем цены аналогов
|
// Добавляем предложения аналогов
|
||||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
Object.values(loadedAnalogs).forEach((analog: any) => {
|
||||||
analog.internalOffers?.forEach((offer: any) => {
|
analog.internalOffers?.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push({
|
||||||
|
...offer,
|
||||||
|
deliveryDuration: offer.deliveryDays
|
||||||
|
});
|
||||||
});
|
});
|
||||||
analog.externalOffers?.forEach((offer: any) => {
|
analog.externalOffers?.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push({
|
||||||
|
...offer,
|
||||||
|
deliveryDuration: offer.deliveryTime
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (prices.length > 0) {
|
// Фильтр по цене - только если есть предложения с разными ценами
|
||||||
|
const prices: number[] = [];
|
||||||
|
allAvailableOffers.forEach((offer: any) => {
|
||||||
|
if (offer.price > 0) prices.push(offer.price);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prices.length > 1) {
|
||||||
const minPrice = Math.min(...prices);
|
const minPrice = Math.min(...prices);
|
||||||
const maxPrice = Math.max(...prices);
|
const maxPrice = Math.max(...prices);
|
||||||
|
|
||||||
@ -87,26 +101,14 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по сроку доставки
|
// Фильтр по сроку доставки - только если есть предложения с разными сроками
|
||||||
const deliveryDays: number[] = [];
|
const deliveryDays: number[] = [];
|
||||||
result.internalOffers?.forEach((offer: any) => {
|
allAvailableOffers.forEach((offer: any) => {
|
||||||
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
|
const days = offer.deliveryDays || offer.deliveryTime || offer.deliveryDuration;
|
||||||
});
|
if (days && days > 0) deliveryDays.push(days);
|
||||||
result.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем сроки доставки аналогов
|
|
||||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
|
||||||
analog.internalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
|
|
||||||
});
|
|
||||||
analog.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deliveryDays.length > 0) {
|
if (deliveryDays.length > 1) {
|
||||||
const minDays = Math.min(...deliveryDays);
|
const minDays = Math.min(...deliveryDays);
|
||||||
const maxDays = Math.max(...deliveryDays);
|
const maxDays = Math.max(...deliveryDays);
|
||||||
|
|
||||||
@ -120,26 +122,13 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по количеству наличия
|
// Фильтр по количеству наличия - только если есть предложения с разными количествами
|
||||||
const quantities: number[] = [];
|
const quantities: number[] = [];
|
||||||
result.internalOffers?.forEach((offer: any) => {
|
allAvailableOffers.forEach((offer: any) => {
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
||||||
});
|
});
|
||||||
result.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем количества аналогов
|
|
||||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
|
||||||
analog.internalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
|
||||||
});
|
|
||||||
analog.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (quantities.length > 0) {
|
if (quantities.length > 1) {
|
||||||
const minQuantity = Math.min(...quantities);
|
const minQuantity = Math.min(...quantities);
|
||||||
const maxQuantity = Math.max(...quantities);
|
const maxQuantity = Math.max(...quantities);
|
||||||
|
|
||||||
@ -163,35 +152,24 @@ const getBestOffers = (offers: any[]) => {
|
|||||||
if (validOffers.length === 0) return [];
|
if (validOffers.length === 0) return [];
|
||||||
|
|
||||||
const result: { offer: any; type: string }[] = [];
|
const result: { offer: any; type: string }[] = [];
|
||||||
const usedOfferIds = new Set<string>();
|
|
||||||
|
|
||||||
// 1. Самая низкая цена (среди всех предложений)
|
// 1. Самая низкая цена (среди всех предложений)
|
||||||
const lowestPriceOffer = [...validOffers].sort((a, b) => a.price - b.price)[0];
|
const lowestPriceOffer = [...validOffers].sort((a, b) => a.price - b.price)[0];
|
||||||
if (lowestPriceOffer) {
|
if (lowestPriceOffer) {
|
||||||
result.push({ offer: lowestPriceOffer, type: 'Самая низкая цена' });
|
result.push({ offer: lowestPriceOffer, type: 'Самая низкая цена' });
|
||||||
usedOfferIds.add(`${lowestPriceOffer.articleNumber}-${lowestPriceOffer.price}-${lowestPriceOffer.deliveryDuration}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Самый дешевый аналог (только среди аналогов)
|
// 2. Самый дешевый аналог (только среди аналогов) - всегда показываем если есть аналоги
|
||||||
const analogOffers = validOffers.filter(offer => offer.isAnalog);
|
const analogOffers = validOffers.filter(offer => offer.isAnalog);
|
||||||
if (analogOffers.length > 0) {
|
if (analogOffers.length > 0) {
|
||||||
const cheapestAnalogOffer = [...analogOffers].sort((a, b) => a.price - b.price)[0];
|
const cheapestAnalogOffer = [...analogOffers].sort((a, b) => a.price - b.price)[0];
|
||||||
const analogId = `${cheapestAnalogOffer.articleNumber}-${cheapestAnalogOffer.price}-${cheapestAnalogOffer.deliveryDuration}`;
|
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
|
||||||
|
|
||||||
if (!usedOfferIds.has(analogId)) {
|
|
||||||
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
|
|
||||||
usedOfferIds.add(analogId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Самая быстрая доставка (среди всех предложений)
|
// 3. Самая быстрая доставка (среди всех предложений)
|
||||||
const fastestDeliveryOffer = [...validOffers].sort((a, b) => a.deliveryDuration - b.deliveryDuration)[0];
|
const fastestDeliveryOffer = [...validOffers].sort((a, b) => a.deliveryDuration - b.deliveryDuration)[0];
|
||||||
if (fastestDeliveryOffer) {
|
if (fastestDeliveryOffer) {
|
||||||
const fastestId = `${fastestDeliveryOffer.articleNumber}-${fastestDeliveryOffer.price}-${fastestDeliveryOffer.deliveryDuration}`;
|
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
|
||||||
|
|
||||||
if (!usedOfferIds.has(fastestId)) {
|
|
||||||
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -376,7 +354,101 @@ export default function SearchResult() {
|
|||||||
|
|
||||||
const hasOffers = result && (result.internalOffers.length > 0 || result.externalOffers.length > 0);
|
const hasOffers = result && (result.internalOffers.length > 0 || result.externalOffers.length > 0);
|
||||||
const hasAnalogs = result && result.analogs.length > 0;
|
const hasAnalogs = result && result.analogs.length > 0;
|
||||||
const searchResultFilters = createFilters(result, loadedAnalogs);
|
|
||||||
|
// Создаем динамические фильтры на основе доступных данных с учетом активных фильтров
|
||||||
|
const searchResultFilters = useMemo(() => {
|
||||||
|
const baseFilters = createFilters(result, loadedAnalogs);
|
||||||
|
|
||||||
|
// Если нет активных фильтров, возвращаем базовые фильтры
|
||||||
|
if (!filtersAreActive) {
|
||||||
|
return baseFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем динамические фильтры с учетом других активных фильтров
|
||||||
|
return baseFilters.map(filter => {
|
||||||
|
if (filter.type !== 'range') {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для каждого диапазонного фильтра пересчитываем границы на основе
|
||||||
|
// предложений, отфильтрованных другими фильтрами (исключая текущий)
|
||||||
|
let relevantOffers = allOffers;
|
||||||
|
|
||||||
|
// Применяем все фильтры кроме текущего
|
||||||
|
relevantOffers = allOffers.filter(offer => {
|
||||||
|
// Фильтр по бренду (если это не фильтр производителя)
|
||||||
|
if (filter.title !== 'Производитель' && selectedBrands.length > 0 && !selectedBrands.includes(offer.brand)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Фильтр по цене (если это не фильтр цены)
|
||||||
|
if (filter.title !== 'Цена (₽)' && priceRange && (offer.price < priceRange[0] || offer.price > priceRange[1])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Фильтр по сроку доставки (если это не фильтр доставки)
|
||||||
|
if (filter.title !== 'Срок доставки (дни)' && deliveryRange) {
|
||||||
|
const deliveryDays = offer.deliveryDuration;
|
||||||
|
if (deliveryDays < deliveryRange[0] || deliveryDays > deliveryRange[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Фильтр по количеству (если это не фильтр количества)
|
||||||
|
if (filter.title !== 'Количество (шт.)' && quantityRange) {
|
||||||
|
const quantity = offer.quantity;
|
||||||
|
if (quantity < quantityRange[0] || quantity > quantityRange[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Фильтр по поисковой строке
|
||||||
|
if (filterSearchTerm) {
|
||||||
|
const searchTerm = filterSearchTerm.toLowerCase();
|
||||||
|
const brandMatch = offer.brand.toLowerCase().includes(searchTerm);
|
||||||
|
const articleMatch = offer.articleNumber.toLowerCase().includes(searchTerm);
|
||||||
|
const nameMatch = offer.name.toLowerCase().includes(searchTerm);
|
||||||
|
if (!brandMatch && !articleMatch && !nameMatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Пересчитываем диапазон на основе отфильтрованных предложений
|
||||||
|
if (filter.title === 'Цена (₽)') {
|
||||||
|
const prices = relevantOffers.filter(o => o.price > 0).map(o => o.price);
|
||||||
|
if (prices.length > 0) {
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
min: Math.floor(Math.min(...prices)),
|
||||||
|
max: Math.ceil(Math.max(...prices))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (filter.title === 'Срок доставки (дни)') {
|
||||||
|
const deliveryDays = relevantOffers
|
||||||
|
.map(o => o.deliveryDuration)
|
||||||
|
.filter(d => d && d > 0);
|
||||||
|
if (deliveryDays.length > 0) {
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
min: Math.min(...deliveryDays),
|
||||||
|
max: Math.max(...deliveryDays)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (filter.title === 'Количество (шт.)') {
|
||||||
|
const quantities = relevantOffers
|
||||||
|
.map(o => o.quantity)
|
||||||
|
.filter(q => q && q > 0);
|
||||||
|
if (quantities.length > 0) {
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
min: Math.min(...quantities),
|
||||||
|
max: Math.max(...quantities)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
});
|
||||||
|
}, [result, loadedAnalogs, filtersAreActive, allOffers, selectedBrands, priceRange, deliveryRange, quantityRange, filterSearchTerm]);
|
||||||
|
|
||||||
const bestOffersData = getBestOffers(filteredOffers);
|
const bestOffersData = getBestOffers(filteredOffers);
|
||||||
|
|
||||||
|
|
||||||
@ -406,6 +478,8 @@ export default function SearchResult() {
|
|||||||
}
|
}
|
||||||
}, [q, article, router.query]);
|
}, [q, article, router.query]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Удаляем старую заглушку - теперь обрабатываем все типы поиска
|
// Удаляем старую заглушку - теперь обрабатываем все типы поиска
|
||||||
|
|
||||||
const minPrice = useMemo(() => {
|
const minPrice = useMemo(() => {
|
||||||
|
@ -74,6 +74,8 @@ const VehicleDetailsPage = () => {
|
|||||||
});
|
});
|
||||||
const [selectedNode, setSelectedNode] = useState<any | null>(null);
|
const [selectedNode, setSelectedNode] = useState<any | null>(null);
|
||||||
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
|
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
|
||||||
|
const [selectedParts, setSelectedParts] = useState<Set<string | number>>(new Set());
|
||||||
|
const [highlightedPart, setHighlightedPart] = useState<string | number | null>(null);
|
||||||
|
|
||||||
// Получаем информацию о выбранном автомобиле
|
// Получаем информацию о выбранном автомобиле
|
||||||
const ssdFromQuery = Array.isArray(router.query.ssd) ? router.query.ssd[0] : router.query.ssd;
|
const ssdFromQuery = Array.isArray(router.query.ssd) ? router.query.ssd[0] : router.query.ssd;
|
||||||
@ -138,6 +140,20 @@ const VehicleDetailsPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Получаем детали выбранного узла, если он выбран
|
// Получаем детали выбранного узла, если он выбран
|
||||||
|
console.log('🔍 [vehicleId].tsx - Проверка условий для GET_LAXIMO_UNIT_DETAILS:', {
|
||||||
|
selectedNode: selectedNode ? {
|
||||||
|
unitid: selectedNode.unitid,
|
||||||
|
name: selectedNode.name,
|
||||||
|
hasSsd: !!selectedNode.ssd
|
||||||
|
} : null,
|
||||||
|
skipCondition: !selectedNode,
|
||||||
|
catalogCode: selectedNode?.catalogCode || selectedNode?.catalog || brand,
|
||||||
|
vehicleId: selectedNode?.vehicleId || vehicleId,
|
||||||
|
unitId: selectedNode?.unitid || selectedNode?.unitId,
|
||||||
|
ssd: selectedNode?.ssd || finalSsd || '',
|
||||||
|
finalSsd: finalSsd ? `${finalSsd.substring(0, 50)}...` : 'отсутствует'
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: unitDetailsData,
|
data: unitDetailsData,
|
||||||
loading: unitDetailsLoading,
|
loading: unitDetailsLoading,
|
||||||
@ -155,6 +171,23 @@ const VehicleDetailsPage = () => {
|
|||||||
: { catalogCode: '', vehicleId: '', unitId: '', ssd: '' },
|
: { catalogCode: '', vehicleId: '', unitId: '', ssd: '' },
|
||||||
skip: !selectedNode,
|
skip: !selectedNode,
|
||||||
errorPolicy: 'all',
|
errorPolicy: 'all',
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
notifyOnNetworkStatusChange: true,
|
||||||
|
onCompleted: (data) => {
|
||||||
|
console.log('🔍 [vehicleId].tsx - GET_LAXIMO_UNIT_DETAILS completed:', {
|
||||||
|
detailsCount: data?.laximoUnitDetails?.length || 0,
|
||||||
|
firstDetail: data?.laximoUnitDetails?.[0],
|
||||||
|
allDetails: data?.laximoUnitDetails?.map((detail: any) => ({
|
||||||
|
name: detail.name,
|
||||||
|
oem: detail.oem,
|
||||||
|
codeonimage: detail.codeonimage,
|
||||||
|
attributesCount: detail.attributes?.length || 0
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('❌ [vehicleId].tsx - GET_LAXIMO_UNIT_DETAILS error:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -234,6 +267,22 @@ const VehicleDetailsPage = () => {
|
|||||||
|
|
||||||
const unitDetails = unitDetailsData?.laximoUnitDetails || [];
|
const unitDetails = unitDetailsData?.laximoUnitDetails || [];
|
||||||
|
|
||||||
|
// Детальное логирование данных от API
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (unitDetailsData?.laximoUnitDetails) {
|
||||||
|
console.log('🔍 [vehicleId].tsx - Полные данные unitDetails от API:', {
|
||||||
|
totalParts: unitDetailsData.laximoUnitDetails.length,
|
||||||
|
firstPart: unitDetailsData.laximoUnitDetails[0],
|
||||||
|
allCodeOnImages: unitDetailsData.laximoUnitDetails.map((part: any) => ({
|
||||||
|
name: part.name,
|
||||||
|
codeonimage: part.codeonimage,
|
||||||
|
detailid: part.detailid,
|
||||||
|
oem: part.oem
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [unitDetailsData]);
|
||||||
|
|
||||||
// Логируем ошибки
|
// Логируем ошибки
|
||||||
if (vehicleError) {
|
if (vehicleError) {
|
||||||
console.error('Vehicle GraphQL error:', vehicleError);
|
console.error('Vehicle GraphQL error:', vehicleError);
|
||||||
@ -382,6 +431,9 @@ const VehicleDetailsPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setSelectedNode(node);
|
setSelectedNode(node);
|
||||||
|
// Сброс состояния выбранных деталей при открытии нового узла
|
||||||
|
setSelectedParts(new Set());
|
||||||
|
setHighlightedPart(null);
|
||||||
router.push(
|
router.push(
|
||||||
{ pathname: router.pathname, query: { ...router.query, unitid: node.unitid || node.id } },
|
{ pathname: router.pathname, query: { ...router.query, unitid: node.unitid || node.id } },
|
||||||
undefined,
|
undefined,
|
||||||
@ -391,6 +443,9 @@ const VehicleDetailsPage = () => {
|
|||||||
// Закрыть KnotIn и удалить unitid из URL
|
// Закрыть KnotIn и удалить unitid из URL
|
||||||
const closeKnot = () => {
|
const closeKnot = () => {
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
|
// Сброс состояния выбранных деталей при закрытии узла
|
||||||
|
setSelectedParts(new Set());
|
||||||
|
setHighlightedPart(null);
|
||||||
const { unitid, ...rest } = router.query;
|
const { unitid, ...rest } = router.query;
|
||||||
router.push(
|
router.push(
|
||||||
{ pathname: router.pathname, query: rest },
|
{ pathname: router.pathname, query: rest },
|
||||||
@ -399,6 +454,25 @@ const VehicleDetailsPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчик выбора детали (множественный выбор)
|
||||||
|
const handlePartSelect = (codeOnImage: string | number | null) => {
|
||||||
|
if (codeOnImage === null) return; // Игнорируем null значения
|
||||||
|
setSelectedParts(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(codeOnImage)) {
|
||||||
|
newSet.delete(codeOnImage); // Убираем если уже выбрана
|
||||||
|
} else {
|
||||||
|
newSet.add(codeOnImage); // Добавляем если не выбрана
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик подсветки детали при наведении
|
||||||
|
const handlePartHighlight = (codeOnImage: string | number | null) => {
|
||||||
|
setHighlightedPart(codeOnImage);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MetaTags {...metaData} />
|
<MetaTags {...metaData} />
|
||||||
@ -551,6 +625,9 @@ const VehicleDetailsPage = () => {
|
|||||||
unitId={selectedNode.unitid}
|
unitId={selectedNode.unitid}
|
||||||
unitName={selectedNode.name}
|
unitName={selectedNode.name}
|
||||||
parts={unitDetails}
|
parts={unitDetails}
|
||||||
|
onPartSelect={handlePartSelect}
|
||||||
|
onPartsHighlight={handlePartHighlight}
|
||||||
|
selectedParts={selectedParts}
|
||||||
/>
|
/>
|
||||||
{unitDetailsLoading ? (
|
{unitDetailsLoading ? (
|
||||||
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
||||||
@ -561,6 +638,10 @@ const VehicleDetailsPage = () => {
|
|||||||
parts={unitDetails}
|
parts={unitDetails}
|
||||||
catalogCode={vehicleInfo.catalog}
|
catalogCode={vehicleInfo.catalog}
|
||||||
vehicleId={vehicleInfo.vehicleid}
|
vehicleId={vehicleInfo.vehicleid}
|
||||||
|
highlightedCodeOnImage={highlightedPart}
|
||||||
|
selectedParts={selectedParts}
|
||||||
|
onPartSelect={handlePartSelect}
|
||||||
|
onPartHover={handlePartHighlight}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: 24, textAlign: 'center' }}>Детали не найдены</div>
|
<div style={{ padding: 24, textAlign: 'center' }}>Детали не найдены</div>
|
||||||
|
@ -100,4 +100,281 @@ input[type=number] {
|
|||||||
|
|
||||||
.cookie-consent-enter {
|
.cookie-consent-enter {
|
||||||
animation: slideInFromBottom 0.3s ease-out;
|
animation: slideInFromBottom 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Анимации для тултипов */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zoomIn {
|
||||||
|
from {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-in {
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-0 {
|
||||||
|
animation-name: fadeIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-95 {
|
||||||
|
animation-name: zoomIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-200 {
|
||||||
|
animation-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для кнопок с курсором pointer */
|
||||||
|
button,
|
||||||
|
.cursor-pointer,
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== СОВРЕМЕННЫЕ СТИЛИ ДЛЯ КРАСИВОГО ТУЛТИПА ===== */
|
||||||
|
|
||||||
|
.tooltip-detail-modern {
|
||||||
|
animation: tooltip-modern-fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
filter: drop-shadow(0 25px 50px rgba(0, 0, 0, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content-modern {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 420px;
|
||||||
|
min-width: 280px;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: none;
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-header-modern {
|
||||||
|
background: linear-gradient(135deg, #EC1C24 0%, #DC1C24 100%);
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-header-modern::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-icon {
|
||||||
|
color: white;
|
||||||
|
opacity: 0.9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-title-section {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-title {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-oem-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-oem-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-oem-value {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-body-modern {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-section-title svg {
|
||||||
|
color: #EC1C24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attributes-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-item:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-key {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-note-modern {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-note-text {
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #92400e;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-icon {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-text {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-text div:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-text div:last-child {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltip-modern-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность для мобильных устройств */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.tooltip-content-modern {
|
||||||
|
max-width: 320px;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-header-modern {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-body-modern {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user