checkbox #6

Merged
egortriston merged 1 commits from numbers into main 2025-06-29 15:45:17 +03:00
6 changed files with 217 additions and 51 deletions

View File

@ -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">

View File

@ -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;

View File

@ -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 || ''}
/>
</>
);
};

View File

@ -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 ? (

View File

@ -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;