Добавлено получение информации о деталях из Parts Index и обновлены компоненты для отображения этой информации. Включены новые типы для работы с данными Parts Index.
This commit is contained in:
117
PARTS_INDEX_INTEGRATION.md
Normal file
117
PARTS_INDEX_INTEGRATION.md
Normal 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)
|
||||
- Ленивая загрузка изображений
|
||||
- Обработка ошибок сети
|
@ -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>
|
||||
|
@ -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[],
|
||||
|
@ -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();
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
Reference in New Issue
Block a user