checkbox #6
@ -52,7 +52,7 @@ const BrandSelectionModal: React.FC<BrandSelectionModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
onClick={handleBackdropClick}
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden">
|
<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 изображения
|
// Функция для корректного формирования URL изображения
|
||||||
const getImageUrl = (baseUrl: string, size: string) => {
|
const getImageUrl = (baseUrl: string, size: string) => {
|
||||||
@ -11,26 +34,139 @@ const getImageUrl = (baseUrl: string, size: string) => {
|
|||||||
.replace('%size%', size);
|
.replace('%size%', size);
|
||||||
};
|
};
|
||||||
|
|
||||||
const KnotIn = ({ node }: { node: any }) => {
|
const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, unitName, parts }) => {
|
||||||
if (!node) return null;
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
let imageUrl = '';
|
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
||||||
if (node.imageurl) {
|
const selectedImageSize = 'source';
|
||||||
imageUrl = getImageUrl(node.imageurl, '250');
|
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||||
} else if (node.largeimageurl) {
|
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
|
||||||
imageUrl = node.largeimageurl;
|
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 (
|
return (
|
||||||
<div className="knotin">
|
<>
|
||||||
{imageUrl ? (
|
<div className="relative inline-block">
|
||||||
<img src={imageUrl} loading="lazy" alt={node.name || "Изображение узла"} className="image-26" />
|
{/* ВРЕМЕННО: выводим количество точек для быстрой проверки */}
|
||||||
) : (
|
<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={{ width: 200, height: 200, background: '#eee', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
{coordinates.length} точек
|
||||||
Нет изображения
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<img
|
||||||
{/* <div style={{ marginTop: 8, fontWeight: 500 }}>{node.name}</div> */}
|
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>
|
||||||
);
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Модалка выбора бренда */}
|
||||||
|
<BrandSelectionModal
|
||||||
|
isOpen={isBrandModalOpen}
|
||||||
|
onClose={() => setIsBrandModalOpen(false)}
|
||||||
|
articleNumber={selectedDetail?.oem || ''}
|
||||||
|
detailName={selectedDetail?.name || ''}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default KnotIn;
|
export default KnotIn;
|
@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import BrandSelectionModal from '../BrandSelectionModal';
|
||||||
|
|
||||||
interface KnotPartsProps {
|
interface KnotPartsProps {
|
||||||
parts: Array<{
|
parts: Array<{
|
||||||
@ -13,14 +14,30 @@ interface KnotPartsProps {
|
|||||||
note?: string;
|
note?: string;
|
||||||
attributes?: Array<{ key: string; name?: string; value: string }>;
|
attributes?: Array<{ key: string; name?: string; value: string }>;
|
||||||
}>;
|
}>;
|
||||||
|
selectedCodeOnImage?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
|
const KnotParts: React.FC<KnotPartsProps> = ({ parts, selectedCodeOnImage }) => {
|
||||||
const router = useRouter();
|
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 (
|
return (
|
||||||
|
<>
|
||||||
<div className="knot-parts">
|
<div className="knot-parts">
|
||||||
{parts.map((part, idx) => (
|
{parts.map((part, idx) => {
|
||||||
<div className="w-layout-hflex knotlistitem" key={part.detailid || 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="w-layout-hflex flex-block-116">
|
||||||
<div className="nuberlist">{part.codeonimage || idx + 1}</div>
|
<div className="nuberlist">{part.codeonimage || idx + 1}</div>
|
||||||
<div className="oemnuber">{part.oem}</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">
|
<div className="w-layout-hflex flex-block-117">
|
||||||
<button
|
<button
|
||||||
className="button-3 w-button"
|
className="button-3 w-button"
|
||||||
onClick={() => {
|
onClick={() => handlePriceClick(part)}
|
||||||
if (part.oem) {
|
|
||||||
router.push(`/search?q=${encodeURIComponent(part.oem)}&mode=parts`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Цена
|
Цена
|
||||||
</button>
|
</button>
|
||||||
@ -44,8 +57,16 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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-hflex flex-block-13">
|
||||||
<div className="w-layout-vflex flex-block-14-copy-copy">
|
<div className="w-layout-vflex flex-block-14-copy-copy">
|
||||||
<button onClick={() => setSelectedNode(null)} style={{ marginBottom: 16 }}>Назад</button>
|
<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 ? (
|
{unitDetailsLoading ? (
|
||||||
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
||||||
) : unitDetailsError ? (
|
) : unitDetailsError ? (
|
||||||
|
@ -386,15 +386,17 @@ input.input-receiver:focus {
|
|||||||
color: var(--_fonts---color--light-blue-grey);
|
color: var(--_fonts---color--light-blue-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
.knotin {
|
/* .knotin {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.knotin img {
|
.knotin img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
object-fit: contain; /* или cover */
|
object-fit: contain;
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.tabs-menu.w-tab-menu {
|
.tabs-menu.w-tab-menu {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
Reference in New Issue
Block a user