checkbox #6
@ -52,7 +52,7 @@ const BrandSelectionModal: React.FC<BrandSelectionModalProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
className="fixed inset-0 bg-black/10 bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden">
|
||||
|
@ -1,4 +1,27 @@
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useRouter } from 'next/router';
|
||||
import { GET_LAXIMO_UNIT_INFO, GET_LAXIMO_UNIT_IMAGE_MAP } from '@/lib/graphql';
|
||||
import BrandSelectionModal from '../BrandSelectionModal';
|
||||
|
||||
interface KnotInProps {
|
||||
catalogCode: string;
|
||||
vehicleId: string;
|
||||
ssd?: string;
|
||||
unitId: string;
|
||||
unitName?: string;
|
||||
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 }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Функция для корректного формирования URL изображения
|
||||
const getImageUrl = (baseUrl: string, size: string) => {
|
||||
@ -11,26 +34,139 @@ const getImageUrl = (baseUrl: string, size: string) => {
|
||||
.replace('%size%', size);
|
||||
};
|
||||
|
||||
const KnotIn = ({ node }: { node: any }) => {
|
||||
if (!node) return null;
|
||||
let imageUrl = '';
|
||||
if (node.imageurl) {
|
||||
imageUrl = getImageUrl(node.imageurl, '250');
|
||||
} else if (node.largeimageurl) {
|
||||
imageUrl = node.largeimageurl;
|
||||
const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, unitName, parts }) => {
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
||||
const selectedImageSize = 'source';
|
||||
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
// Получаем инфо об узле (для картинки)
|
||||
const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery(
|
||||
GET_LAXIMO_UNIT_INFO,
|
||||
{
|
||||
variables: { catalogCode, vehicleId, unitId, ssd: ssd || '' },
|
||||
skip: !catalogCode || !vehicleId || !unitId,
|
||||
errorPolicy: 'all',
|
||||
}
|
||||
);
|
||||
// Получаем карту координат
|
||||
const { data: imageMapData, loading: imageMapLoading, error: imageMapError } = useQuery(
|
||||
GET_LAXIMO_UNIT_IMAGE_MAP,
|
||||
{
|
||||
variables: { catalogCode, vehicleId, unitId, ssd: ssd || '' },
|
||||
skip: !catalogCode || !vehicleId || !unitId,
|
||||
errorPolicy: 'all',
|
||||
}
|
||||
);
|
||||
|
||||
const unitInfo = unitInfoData?.laximoUnitInfo;
|
||||
const coordinates = imageMapData?.laximoUnitImageMap?.coordinates || [];
|
||||
const imageUrl = unitInfo?.imageurl ? getImageUrl(unitInfo.imageurl, selectedImageSize) : '';
|
||||
|
||||
// Масштабируем точки после загрузки картинки
|
||||
const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const img = e.currentTarget;
|
||||
if (!img.naturalWidth || !img.naturalHeight) return;
|
||||
setImageScale({
|
||||
x: img.offsetWidth / img.naturalWidth,
|
||||
y: img.offsetHeight / img.naturalHeight,
|
||||
});
|
||||
};
|
||||
|
||||
// Клик по точке: найти part по codeonimage/detailid и открыть BrandSelectionModal
|
||||
const handlePointClick = (codeonimage: string | number) => {
|
||||
if (!parts) return;
|
||||
console.log('Клик по точке:', codeonimage, 'Все детали:', parts);
|
||||
const part = parts.find(
|
||||
(p) =>
|
||||
(p.codeonimage && p.codeonimage.toString() === codeonimage.toString()) ||
|
||||
(p.detailid && p.detailid.toString() === codeonimage.toString())
|
||||
);
|
||||
console.log('Найдена деталь для точки:', part);
|
||||
if (part?.oem) {
|
||||
setSelectedDetail({ oem: part.oem, name: part.name || '' });
|
||||
setIsBrandModalOpen(true);
|
||||
} else {
|
||||
console.warn('Нет артикула (oem) для выбранной точки:', codeonimage, part);
|
||||
}
|
||||
};
|
||||
|
||||
// Для отладки: вывести детали и координаты
|
||||
React.useEffect(() => {
|
||||
console.log('KnotIn parts:', parts);
|
||||
console.log('KnotIn coordinates:', coordinates);
|
||||
}, [parts, coordinates]);
|
||||
|
||||
if (unitInfoLoading || imageMapLoading) {
|
||||
return <div className="text-center py-8 text-gray-500">Загружаем схему узла...</div>;
|
||||
}
|
||||
if (unitInfoError) {
|
||||
return <div className="text-center py-8 text-red-600">Ошибка загрузки схемы: {unitInfoError.message}</div>;
|
||||
}
|
||||
if (!imageUrl) {
|
||||
return <div className="text-center py-8 text-gray-400">Нет изображения для этого узла</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="knotin">
|
||||
{imageUrl ? (
|
||||
<img src={imageUrl} loading="lazy" alt={node.name || "Изображение узла"} className="image-26" />
|
||||
) : (
|
||||
<div style={{ width: 200, height: 200, background: '#eee', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
Нет изображения
|
||||
<>
|
||||
<div className="relative inline-block">
|
||||
{/* ВРЕМЕННО: выводим количество точек для быстрой проверки */}
|
||||
<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} точек
|
||||
</div>
|
||||
)}
|
||||
{/* <div style={{ marginTop: 8, fontWeight: 500 }}>{node.name}</div> */}
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={imageUrl}
|
||||
loading="lazy"
|
||||
alt={unitName || unitInfo?.name || "Изображение узла"}
|
||||
onLoad={handleImageLoad}
|
||||
className="max-w-full h-auto mx-auto rounded"
|
||||
style={{ maxWidth: 400, display: 'block' }}
|
||||
/>
|
||||
{/* Точки/области */}
|
||||
{coordinates.map((coord: any, idx: number) => {
|
||||
const scaledX = coord.x * imageScale.x;
|
||||
const scaledY = coord.y * imageScale.y;
|
||||
const scaledWidth = coord.width * imageScale.x;
|
||||
const scaledHeight = coord.height * imageScale.y;
|
||||
return (
|
||||
<div
|
||||
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
||||
tabIndex={0}
|
||||
aria-label={`Деталь ${coord.codeonimage}`}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
|
||||
}}
|
||||
className="absolute flex items-center justify-center border-2 border-red-600 bg-white rounded-full cursor-pointer"
|
||||
style={{
|
||||
left: scaledX,
|
||||
top: scaledY,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
title={coord.codeonimage}
|
||||
onClick={() => handlePointClick(coord.codeonimage)}
|
||||
>
|
||||
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none">
|
||||
{coord.codeonimage}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* Модалка выбора бренда */}
|
||||
<BrandSelectionModal
|
||||
isOpen={isBrandModalOpen}
|
||||
onClose={() => setIsBrandModalOpen(false)}
|
||||
articleNumber={selectedDetail?.oem || ''}
|
||||
detailName={selectedDetail?.name || ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnotIn;
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import BrandSelectionModal from '../BrandSelectionModal';
|
||||
|
||||
interface KnotPartsProps {
|
||||
parts: Array<{
|
||||
@ -13,14 +14,30 @@ interface KnotPartsProps {
|
||||
note?: string;
|
||||
attributes?: Array<{ key: string; name?: string; value: string }>;
|
||||
}>;
|
||||
selectedCodeOnImage?: string | number;
|
||||
}
|
||||
|
||||
const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
|
||||
const router = useRouter();
|
||||
const KnotParts: React.FC<KnotPartsProps> = ({ parts, selectedCodeOnImage }) => {
|
||||
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
|
||||
|
||||
const handlePriceClick = (part: any) => {
|
||||
if (part.oem) {
|
||||
setSelectedDetail({ oem: part.oem, name: part.name || '' });
|
||||
setIsBrandModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="knot-parts">
|
||||
{parts.map((part, idx) => (
|
||||
<div className="w-layout-hflex knotlistitem" key={part.detailid || idx}>
|
||||
{parts.map((part, idx) => {
|
||||
const isSelected = part.codeonimage && part.codeonimage === selectedCodeOnImage;
|
||||
return (
|
||||
<div
|
||||
className={`w-layout-hflex knotlistitem border rounded transition-colors duration-150 ${isSelected ? 'bg-yellow-100 border-yellow-400' : 'border-transparent'}`}
|
||||
key={part.detailid || idx}
|
||||
>
|
||||
<div className="w-layout-hflex flex-block-116">
|
||||
<div className="nuberlist">{part.codeonimage || idx + 1}</div>
|
||||
<div className="oemnuber">{part.oem}</div>
|
||||
@ -29,11 +46,7 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
|
||||
<div className="w-layout-hflex flex-block-117">
|
||||
<button
|
||||
className="button-3 w-button"
|
||||
onClick={() => {
|
||||
if (part.oem) {
|
||||
router.push(`/search?q=${encodeURIComponent(part.oem)}&mode=parts`);
|
||||
}
|
||||
}}
|
||||
onClick={() => handlePriceClick(part)}
|
||||
>
|
||||
Цена
|
||||
</button>
|
||||
@ -44,8 +57,16 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<BrandSelectionModal
|
||||
isOpen={isBrandModalOpen}
|
||||
onClose={() => setIsBrandModalOpen(false)}
|
||||
articleNumber={selectedDetail?.oem || ''}
|
||||
detailName={selectedDetail?.name || ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -303,7 +303,14 @@ const VehicleDetailsPage = () => {
|
||||
<div className="w-layout-hflex flex-block-13">
|
||||
<div className="w-layout-vflex flex-block-14-copy-copy">
|
||||
<button onClick={() => setSelectedNode(null)} style={{ marginBottom: 16 }}>Назад</button>
|
||||
<KnotIn node={selectedNode} />
|
||||
<KnotIn
|
||||
catalogCode={vehicleInfo.catalog}
|
||||
vehicleId={vehicleInfo.vehicleid}
|
||||
ssd={vehicleInfo.ssd}
|
||||
unitId={selectedNode.unitid}
|
||||
unitName={selectedNode.name}
|
||||
parts={unitDetails}
|
||||
/>
|
||||
{unitDetailsLoading ? (
|
||||
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
||||
) : unitDetailsError ? (
|
||||
|
@ -386,15 +386,17 @@ input.input-receiver:focus {
|
||||
color: var(--_fonts---color--light-blue-grey);
|
||||
}
|
||||
|
||||
.knotin {
|
||||
/* .knotin {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.knotin img {
|
||||
max-width: 100%;
|
||||
object-fit: contain; /* или cover */
|
||||
}
|
||||
object-fit: contain;
|
||||
} */
|
||||
|
||||
|
||||
|
||||
.tabs-menu.w-tab-menu {
|
||||
scrollbar-width: none;
|
||||
|
Reference in New Issue
Block a user