Добавлено получение информации о деталях из Parts Index и обновлены компоненты для отображения этой информации. Включены новые типы для работы с данными Parts Index.

This commit is contained in:
Bivekich
2025-06-27 15:31:48 +03:00
parent d44874775c
commit 855018bd6c
6 changed files with 258 additions and 4 deletions

117
PARTS_INDEX_INTEGRATION.md Normal file
View File

@ -0,0 +1,117 @@
# Интеграция Parts Index API
## Описание
В проект добавлена интеграция с Parts Index API для отображения детальной информации о деталях, включая:
- Главную фотографию детали
- Штрих-коды
- Технические характеристики
- Категории
- Дополнительные изображения
- Логотип "powered by Parts Index"
## Реализованные компоненты
### 1. Типы (`src/types/partsindex.ts`)
- `PartsIndexEntityInfo` - информация о детали
- `PartsIndexEntityInfoResponse` - ответ API
- `PartsIndexEntityInfoVariables` - параметры запроса
### 2. Сервис (`src/lib/partsindex-service.ts`)
- `getEntityInfo(code, brand?, lang?)` - получение информации о детали
### 3. Хук (`src/hooks/usePartsIndex.ts`)
- `usePartsIndexEntityInfo(code, brand)` - хук для получения данных
### 4. Компонент (`src/components/PartsIndexCard.tsx`)
- Отображение карточки с информацией о детали
- Поддержка состояния загрузки
- Адаптивный дизайн
## Интеграция в страницу поиска
В файле `src/pages/search-result.tsx` добавлено:
```tsx
import PartsIndexCard from "@/components/PartsIndexCard";
import { usePartsIndexEntityInfo } from "@/hooks/usePartsIndex";
// В компоненте:
const { entityInfo, loading: partsIndexLoading } = usePartsIndexEntityInfo(
searchQuery || null,
brandQuery || null
);
// В JSX:
{partsIndexLoading && (
<PartsIndexCard
entityInfo={null as any}
loading={true}
/>
)}
{entityInfo && !partsIndexLoading && (
<PartsIndexCard
entityInfo={entityInfo}
loading={false}
/>
)}
```
## API Parts Index
### Endpoint
```
GET https://api.parts-index.com/v1/entities
```
### Параметры
- `code` (обязательный) - артикул детали
- `brand` (опциональный) - бренд
- `lang` (опциональный) - язык (по умолчанию 'ru')
### Заголовки
```
Authorization: PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE
Accept: application/json
```
### Пример запроса
```bash
curl -H "Authorization: PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE" \
"https://api.parts-index.com/v1/entities?code=059198405B&brand=VAG&lang=ru"
```
## Тестирование
### URL для тестирования
```
http://localhost:3002/search-result?article=059198405B&brand=VAG
```
### Тестовая HTML страница
Создана страница `test-parts-index.html` для демонстрации работы API без React.
## Функциональность
1. **Автоматическая загрузка** - при переходе на страницу результатов поиска
2. **Главная фотография** - отображается первое изображение из массива
3. **Логотип Parts Index** - в правом верхнем углу карточки
4. **Характеристики** - первые 6 параметров из API
5. **Штрих-коды** - все доступные штрих-коды
6. **Дополнительные изображения** - до 4 дополнительных фото
7. **Обработка ошибок** - скрытие изображений при ошибке загрузки
## Стили
Компонент использует Tailwind CSS классы для стилизации:
- Адаптивная сетка для характеристик
- Скроллинг для дополнительных изображений
- Состояние загрузки с анимацией
- Обработка ошибок изображений
## Производительность
- Загрузка данных только при наличии артикула
- Кэширование на уровне React Query (через Apollo Client)
- Ленивая загрузка изображений
- Обработка ошибок сети

View File

@ -30,6 +30,7 @@ interface CoreProductCardProps {
isAnalog?: boolean;
isLoadingOffers?: boolean;
onLoadOffers?: () => void;
partsIndexPowered?: boolean;
}
const CoreProductCard: React.FC<CoreProductCardProps> = ({
@ -41,7 +42,8 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
showMoreText,
isAnalog = false,
isLoadingOffers = false,
onLoadOffers
onLoadOffers,
partsIndexPowered = false
}) => {
const { addItem } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
@ -196,6 +198,11 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
{image && (
<div className="div-block-20">
<img src={image} loading="lazy" alt={name} className="image-10" />
{partsIndexPowered && (
<div className="text-xs text-gray-500 mt-1 text-center">
powered by <span className="font-semibold text-blue-600">Parts Index</span>
</div>
)}
</div>
)}
</div>
@ -246,6 +253,11 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
{image && (
<div className="div-block-20">
<img src={image} loading="lazy" alt={name} className="image-10" />
{partsIndexPowered && (
<div className="text-xs text-gray-500 mt-1 text-center">
powered by <span className="font-semibold text-blue-600">Parts Index</span>
</div>
)}
</div>
)}
</div>

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { partsIndexService } from '@/lib/partsindex-service';
import { PartsIndexCatalog, PartsIndexGroup, PartsIndexTabData } from '@/types/partsindex';
import { PartsIndexCatalog, PartsIndexGroup, PartsIndexTabData, PartsIndexEntityInfo } from '@/types/partsindex';
export const usePartsIndexCatalogs = () => {
const [catalogs, setCatalogs] = useState<PartsIndexCatalog[]>([]);
@ -59,6 +59,44 @@ export const usePartsIndexCatalogGroups = (catalogId: string | null) => {
return { group, loading, error };
};
export const usePartsIndexEntityInfo = (code: string | null, brand?: string | null) => {
const [entityInfo, setEntityInfo] = useState<PartsIndexEntityInfo | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!code) {
setEntityInfo(null);
return;
}
const fetchEntityInfo = async () => {
try {
setLoading(true);
setError(null);
const response = await partsIndexService.getEntityInfo(code, brand || undefined, 'ru');
// Берем первый элемент из списка, если он есть
if (response.list && response.list.length > 0) {
setEntityInfo(response.list[0]);
} else {
setEntityInfo(null);
}
} catch (err) {
setError(err as Error);
console.error(`Ошибка загрузки информации о детали ${code}:`, err);
setEntityInfo(null);
} finally {
setLoading(false);
}
};
fetchEntityInfo();
}, [code, brand]);
return { entityInfo, loading, error };
};
// Функция для преобразования данных Parts Index в формат меню
export const transformPartsIndexToTabData = (
catalogs: PartsIndexCatalog[],

View File

@ -1,4 +1,4 @@
import { PartsIndexCatalogsResponse, PartsIndexGroup } from '@/types/partsindex';
import { PartsIndexCatalogsResponse, PartsIndexGroup, PartsIndexEntityInfoResponse } from '@/types/partsindex';
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com';
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
@ -54,6 +54,41 @@ class PartsIndexService {
throw error;
}
}
/**
* Получить информацию о детали по артикулу и бренду
*/
async getEntityInfo(code: string, brand?: string, lang: string = 'ru'): Promise<PartsIndexEntityInfoResponse> {
try {
const params = new URLSearchParams({
code: code,
lang: lang
});
if (brand) {
params.append('brand', brand);
}
const response = await fetch(
`${PARTS_INDEX_API_BASE}/v1/entities?${params.toString()}`,
{
headers: {
'Accept': 'application/json',
'Authorization': API_KEY,
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Ошибка получения информации о детали ${code}:`, error);
throw error;
}
}
}
export const partsIndexService = new PartsIndexService();

View File

@ -15,6 +15,7 @@ import CatalogSortDropdown from "@/components/CatalogSortDropdown";
import MobileMenuBottomSection from '../components/MobileMenuBottomSection';
import { SEARCH_PRODUCT_OFFERS, GET_ANALOG_OFFERS } from "@/lib/graphql";
import { useArticleImage } from "@/hooks/useArticleImage";
import { usePartsIndexEntityInfo } from "@/hooks/usePartsIndex";
const ANALOGS_CHUNK_SIZE = 5;
@ -255,6 +256,12 @@ export default function SearchResult() {
});
const { imageUrl: mainImageUrl } = useArticleImage(artId as string, { enabled: !!artId });
// Получаем информацию о детали из Parts Index
const { entityInfo, loading: partsIndexLoading } = usePartsIndexEntityInfo(
searchQuery || null,
brandQuery || null
);
const [
getAnalogOffers,
@ -568,15 +575,20 @@ export default function SearchResult() {
return null;
}
// Используем фотографию из Parts Index, если она есть, иначе fallback на mainImageUrl
const partsIndexImage = entityInfo?.images?.[0];
const displayImage = partsIndexImage || mainImageUrl;
return (
<>
<CoreProductCard
brand={result.brand}
article={result.articleNumber}
name={result.name}
image={mainImageUrl}
image={displayImage}
offers={mainProductOffers}
showMoreText={mainProductOffers.length < filteredOffers.filter(o => !o.isAnalog).length ? "Показать еще" : undefined}
partsIndexPowered={!!partsIndexImage}
/>
</>
);

View File

@ -152,4 +152,44 @@ export interface PartsIndexParamsVariables {
generationId?: string;
params?: string;
q?: string;
}
// Типы для получения информации о детали по артикулу
export interface PartsIndexEntityInfo {
id: string;
name: PartsIndexProductName;
originalName: string;
code: string;
barcodes: string[];
brand: PartsIndexBrand;
description: string;
parameters: {
id: string;
name: string;
params: PartsIndexParameter[];
}[];
images: string[];
links: any[];
groups: {
main: Array<{
id: string;
name: string;
level: number;
}>;
additional: Array<Array<{
id: string;
name: string;
level: number;
}>>;
};
}
export interface PartsIndexEntityInfoResponse {
list: PartsIndexEntityInfo[];
}
export interface PartsIndexEntityInfoVariables {
code: string;
brand?: string;
lang?: string;
}