Добавлено получение навигационных категорий с иконками и обновление логики отображения иконок в компоненте BottomHead. Обновлены типы данных и стили для навигационных иконок. Оптимизирована загрузка групп для категорий в компоненте BottomHeadPartsIndex.

This commit is contained in:
Bivekich
2025-07-13 21:42:06 +03:00
parent 132e39b87e
commit ad5dcc03e3
6 changed files with 406 additions and 136 deletions

View File

@ -2,8 +2,9 @@ import React, { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql'; import { GET_PARTSINDEX_CATEGORIES, GET_NAVIGATION_CATEGORIES } from '@/lib/graphql';
import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex'; import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex';
import { NavigationCategory } from '@/types';
function useIsMobile(breakpoint = 767) { function useIsMobile(breakpoint = 767) {
const [isMobile, setIsMobile] = React.useState(false); const [isMobile, setIsMobile] = React.useState(false);
@ -111,6 +112,22 @@ const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => {
return transformed; return transformed;
}; };
// Функция для поиска иконки для категории
const findCategoryIcon = (catalogId: string, navigationCategories: NavigationCategory[]): string | null => {
console.log('🔍 Ищем иконку для catalogId:', catalogId);
console.log('📋 Доступные навигационные категории:', navigationCategories);
// Ищем навигационную категорию для данного каталога (без группы)
const categoryIcon = navigationCategories.find(
nav => nav.partsIndexCatalogId === catalogId && (!nav.partsIndexGroupId || nav.partsIndexGroupId === '')
);
console.log('🎯 Найденная категория:', categoryIcon);
console.log('🖼️ Возвращаемая иконка:', categoryIcon?.icon || null);
return categoryIcon?.icon || null;
};
const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => { const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
@ -155,6 +172,20 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
} }
); );
// Получаем навигационные категории с иконками
const { data: navigationData, loading: navigationLoading, error: navigationError } = useQuery<{ navigationCategories: NavigationCategory[] }>(
GET_NAVIGATION_CATEGORIES,
{
errorPolicy: 'all',
onCompleted: (data) => {
console.log('🎉 Навигационные категории получены:', data);
},
onError: (error) => {
console.error('❌ Ошибка загрузки навигационных категорий:', error);
}
}
);
// Обновляем данные табов когда получаем данные от API // Обновляем данные табов когда получаем данные от API
useEffect(() => { useEffect(() => {
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) { if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
@ -356,27 +387,80 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
<div className="w-layout-hflex flex-block-90"> <div className="w-layout-hflex flex-block-90">
<div className="w-layout-vflex flex-block-88" style={{ maxHeight: "60vh", overflowY: "auto" }}> <div className="w-layout-vflex flex-block-88" style={{ maxHeight: "60vh", overflowY: "auto" }}>
{/* Меню с иконками - показываем все категории из API */} {/* Меню с иконками - показываем все категории из API */}
{tabData.map((tab, idx) => ( {tabData.map((tab, idx) => {
<a // Получаем catalogId для поиска иконки
href="#" const catalogId = catalogsData?.partsIndexCategoriesWithGroups?.[idx]?.id || `fallback_${idx}`;
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`} console.log(`🏷️ Обрабатываем категорию ${idx}: "${tab.label}" с catalogId: "${catalogId}"`);
key={tab.label} const icon = navigationData?.navigationCategories ? findCategoryIcon(catalogId, navigationData.navigationCategories) : null;
onClick={() => { console.log(`🎨 Для категории "${tab.label}" будет показана ${icon ? 'иконка: ' + icon : 'звездочка (fallback)'}`);
setActiveTabIndex(idx);
}} return (
style={{ cursor: "pointer" }} <a
> href="#"
<div className="div-block-29"> className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
<div className="code-embed-12 w-embed"> key={tab.label}
{/* SVG-звезда */} onClick={() => {
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"> setActiveTabIndex(idx);
<path d="M10.3158 0.643914C10.4674 0.365938 10.8666 0.365938 11.0182 0.643914L14.0029 6.11673C14.0604 6.22222 14.1623 6.29626 14.2804 6.31838L20.4077 7.46581C20.7189 7.52409 20.8423 7.9037 20.6247 8.13378L16.3421 12.6636C16.2595 12.7509 16.2206 12.8707 16.2361 12.9899L17.0382 19.1718C17.079 19.4858 16.7561 19.7204 16.47 19.5847L10.8385 16.9114C10.73 16.8599 10.604 16.8599 10.4955 16.9114L4.86394 19.5847C4.5779 19.7204 4.25499 19.4858 4.29573 19.1718L5.0979 12.9899C5.11336 12.8707 5.07444 12.7509 4.99189 12.6636L0.709252 8.13378C0.491728 7.9037 0.615069 7.52409 0.926288 7.46581L7.05357 6.31838C7.17168 6.29626 7.27358 6.22222 7.33112 6.11673L10.3158 0.643914Z" fill="CurrentColor"></path> }}
</svg> style={{ cursor: "pointer" }}
>
<div className="div-block-29">
<div className="code-embed-12 w-embed">
{(() => {
console.log(`🎭 Рендеринг иконки для "${tab.label}": ${icon ? 'IMG' : 'SVG'}`);
console.log(`🔍 icon значение:`, icon);
return null;
})()}
{/* Условный рендеринг: либо IMG, либо SVG */}
{icon ? (
<img
src={icon}
alt={tab.label}
width="21"
height="20"
className="navigation-icon-img"
style={{
display: 'block',
position: 'relative',
zIndex: 10,
border: '3px solid lime',
backgroundColor: 'magenta',
objectFit: 'contain'
}}
onLoad={(e) => {
console.log('✅ Иконка успешно загружена:', icon);
console.log('✅ Элемент img:', e.currentTarget);
}}
onError={(e) => {
console.log('❌ Ошибка загрузки иконки:', icon);
console.log('❌ Элемент img с ошибкой:', e.currentTarget);
}}
/>
) : (
/* SVG-звезда как fallback */
<svg
width="21"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
display: 'block',
position: 'relative',
zIndex: 5,
border: '2px solid blue' // Временно для отладки
}}
>
<path d="M10.3158 0.643914C10.4674 0.365938 10.8666 0.365938 11.0182 0.643914L14.0029 6.11673C14.0604 6.22222 14.1623 6.29626 14.2804 6.31838L20.4077 7.46581C20.7189 7.52409 20.8423 7.9037 20.6247 8.13378L16.3421 12.6636C16.2595 12.7509 16.2206 12.8707 16.2361 12.9899L17.0382 19.1718C17.079 19.4858 16.7561 19.7204 16.47 19.5847L10.8385 16.9114C10.73 16.8599 10.604 16.8599 10.4955 16.9114L4.86394 19.5847C4.5779 19.7204 4.25499 19.4858 4.29573 19.1718L5.0979 12.9899C5.11336 12.8707 5.07444 12.7509 4.99189 12.6636L0.709252 8.13378C0.491728 7.9037 0.615069 7.52409 0.926288 7.46581L7.05357 6.31838C7.17168 6.29626 7.27358 6.22222 7.33112 6.11673L10.3158 0.643914Z" fill="CurrentColor"></path>
</svg>
)}
</div>
</div> </div>
</div> <div className="text-block-47">{tab.label}</div>
<div className="text-block-47">{tab.label}</div> </a>
</a> );
))} })}
</div> </div>
{/* Правая часть меню с подкатегориями и картинками */} {/* Правая часть меню с подкатегориями и картинками */}
<div className="w-layout-vflex flex-block-89"> <div className="w-layout-vflex flex-block-89">

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useQuery, useLazyQuery } from '@apollo/client';
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
function useIsMobile(breakpoint = 767) { function useIsMobile(breakpoint = 767) {
const [isMobile, setIsMobile] = React.useState(false); const [isMobile, setIsMobile] = React.useState(false);
@ -13,35 +14,38 @@ function useIsMobile(breakpoint = 767) {
return isMobile; return isMobile;
} }
// Типы для Parts Index API // Типы данных
interface PartsIndexCatalog {
id: string;
name: string;
image: string;
}
interface PartsIndexEntityName {
id: string;
name: string;
}
interface PartsIndexGroup {
id: string;
name: string;
lang: string;
image: string;
lft: number;
rgt: number;
entityNames: PartsIndexEntityName[];
subgroups: PartsIndexGroup[];
}
interface PartsIndexTabData { interface PartsIndexTabData {
label: string; label: string;
heading: string; heading: string;
links: string[]; links: string[];
catalogId: string; catalogId: string;
group?: PartsIndexGroup; group?: any;
groupsLoaded?: boolean; // флаг что группы загружены
}
interface PartsIndexCatalog {
id: string;
name: string;
image?: string;
groups?: PartsIndexGroup[];
}
interface PartsIndexGroup {
id: string;
name: string;
image?: string;
entityNames?: { id: string; name: string }[];
subgroups?: { id: string; name: string }[];
}
// GraphQL типы
interface PartsIndexCatalogsData {
partsIndexCategoriesWithGroups: PartsIndexCatalog[];
}
interface PartsIndexCatalogsVariables {
lang?: 'ru' | 'en';
} }
// Fallback статичные данные // Fallback статичные данные
@ -51,57 +55,66 @@ const fallbackTabData: PartsIndexTabData[] = [
heading: "Детали ТО", heading: "Детали ТО",
catalogId: "parts_to", catalogId: "parts_to",
links: ["Детали ТО"], links: ["Детали ТО"],
groupsLoaded: false,
}, },
{ {
label: "Масла", label: "Масла",
heading: "Масла", heading: "Масла",
catalogId: "oils", catalogId: "oils",
links: ["Масла"], links: ["Масла"],
groupsLoaded: false,
}, },
{ {
label: "Шины", label: "Шины",
heading: "Шины", heading: "Шины",
catalogId: "tyres", catalogId: "tyres",
links: ["Шины"], links: ["Шины"],
groupsLoaded: false,
}, },
]; ];
// Сервис для работы с Parts Index API // Создаем базовые табы только с названиями каталогов
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com'; const createBaseTabData = (catalogs: PartsIndexCatalog[]): PartsIndexTabData[] => {
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE'; console.log('🔄 Создаем базовые табы из каталогов:', catalogs.length, 'элементов');
return catalogs.map(catalog => ({
label: catalog.name,
heading: catalog.name,
links: [catalog.name], // Изначально показываем только название каталога
catalogId: catalog.id,
groupsLoaded: false, // Группы еще не загружены
}));
};
async function fetchCatalogs(): Promise<PartsIndexCatalog[]> { // Преобразуем данные PartsIndex в формат нашего меню с группами
try { const transformPartsIndexToTabData = (catalog: PartsIndexCatalog): string[] => {
const response = await fetch(`${PARTS_INDEX_API_BASE}/v1/catalogs?lang=ru`, { console.log(`📝 Обрабатываем группы каталога: "${catalog.name}"`);
headers: { 'Accept': 'application/json' },
}); let links: string[] = [];
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json(); if (catalog.groups && catalog.groups.length > 0) {
return data.list; // Для каждой группы проверяем есть ли подгруппы
} catch (error) { catalog.groups.forEach(group => {
console.error('Ошибка получения каталогов Parts Index:', error); if (group.subgroups && group.subgroups.length > 0) {
return []; // Если есть подгруппы, добавляем их названия
} links.push(...group.subgroups.slice(0, 9 - links.length).map(subgroup => subgroup.name));
} } else {
// Если подгрупп нет, добавляем название самой группы
async function fetchCatalogGroup(catalogId: string): Promise<PartsIndexGroup | null> { if (links.length < 9) {
try { links.push(group.name);
const response = await fetch( }
`${PARTS_INDEX_API_BASE}/v1/catalogs/${catalogId}/groups?lang=ru`,
{
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(`Ошибка получения группы каталога ${catalogId}:`, error);
return null;
} }
}
// Если подкатегорий нет, показываем название категории
if (links.length === 0) {
links = [catalog.name];
}
console.log(`🔗 Подкатегории для "${catalog.name}":`, links);
return links.slice(0, 9); // Ограничиваем максимум 9 элементов
};
const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => { const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -109,7 +122,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
const [mobileCategory, setMobileCategory] = useState<null | any>(null); const [mobileCategory, setMobileCategory] = useState<null | any>(null);
const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData); const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData);
const [activeTabIndex, setActiveTabIndex] = useState(0); const [activeTabIndex, setActiveTabIndex] = useState(0);
const [loading, setLoading] = useState(false); const [loadingGroups, setLoadingGroups] = useState<Set<number>>(new Set());
// Пагинация категорий // Пагинация категорий
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
@ -126,52 +139,116 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
} }
}, [menuOpen]); }, [menuOpen]);
// Загрузка каталогов и их групп // Получаем только каталоги PartsIndex (без групп для начальной загрузки)
const { data: catalogsData, loading, error } = useQuery<PartsIndexCatalogsData, PartsIndexCatalogsVariables>(
GET_PARTSINDEX_CATEGORIES,
{
variables: {
lang: 'ru'
},
errorPolicy: 'all',
fetchPolicy: 'cache-first', // Используем кэширование агрессивно
nextFetchPolicy: 'cache-first', // Продолжаем использовать кэш
notifyOnNetworkStatusChange: false,
onCompleted: (data) => {
console.log('🎉 PartsIndex каталоги получены через GraphQL (базовые):', data);
},
onError: (error) => {
console.error('❌ Ошибка загрузки PartsIndex каталогов:', error);
}
}
);
// Ленивый запрос для загрузки групп конкретного каталога
const [loadCatalogGroups, { loading: groupsLoading }] = useLazyQuery<PartsIndexCatalogsData, PartsIndexCatalogsVariables>(
GET_PARTSINDEX_CATEGORIES,
{
errorPolicy: 'all',
fetchPolicy: 'cache-first',
nextFetchPolicy: 'cache-first',
notifyOnNetworkStatusChange: false,
onCompleted: (data) => {
console.log('🎉 Группы каталога загружены:', data);
},
onError: (error) => {
console.error('❌ Ошибка загрузки групп каталога:', error);
}
}
);
// Обновляем базовые данные табов когда получаем каталоги
useEffect(() => { useEffect(() => {
const loadData = async () => { if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
if (tabData === fallbackTabData) { // Загружаем только если еще не загружали console.log('✅ Обновляем базовое меню PartsIndex:', catalogsData.partsIndexCategoriesWithGroups.length, 'каталогов');
setLoading(true);
try { const baseTabData = createBaseTabData(catalogsData.partsIndexCategoriesWithGroups);
console.log('🔄 Загружаем каталоги Parts Index...'); setTabData(baseTabData);
const catalogs = await fetchCatalogs(); setActiveTabIndex(0);
} else if (error) {
console.warn('⚠️ Используем fallback данные из-за ошибки PartsIndex:', error);
setTabData(fallbackTabData);
setActiveTabIndex(0);
}
}, [catalogsData, error]);
// Функция для ленивой загрузки групп при наведении на таб
const loadGroupsForTab = async (tabIndex: number) => {
const tab = tabData[tabIndex];
if (!tab || tab.groupsLoaded || loadingGroups.has(tabIndex)) {
return; // Группы уже загружены или загружаются
}
console.log('🔄 Загружаем группы для каталога:', tab.catalogId);
setLoadingGroups(prev => new Set([...prev, tabIndex]));
try {
const result = await loadCatalogGroups({
variables: {
lang: 'ru'
}
});
if (result.data?.partsIndexCategoriesWithGroups) {
const catalog = result.data.partsIndexCategoriesWithGroups.find(c => c.id === tab.catalogId);
if (catalog) {
const links = transformPartsIndexToTabData(catalog);
if (catalogs.length > 0) { // Обновляем конкретный таб с загруженными группами
console.log(`✅ Получено ${catalogs.length} каталогов`); setTabData(prevTabs => {
const newTabs = [...prevTabs];
// Загружаем группы для первых нескольких каталогов newTabs[tabIndex] = {
const catalogsToLoad = catalogs.slice(0, 10); ...newTabs[tabIndex],
const tabDataPromises = catalogsToLoad.map(async (catalog) => { links,
const group = await fetchCatalogGroup(catalog.id); group: catalog.groups?.[0],
groupsLoaded: true
// Получаем подкатегории из entityNames или повторяем название категории };
const links = group?.entityNames && group.entityNames.length > 0 return newTabs;
? group.entityNames.slice(0, 9).map(entity => entity.name) });
: [catalog.name]; // Если нет подкатегорий, повторяем название категории
return {
label: catalog.name,
heading: catalog.name,
links,
catalogId: catalog.id,
group
};
});
const apiTabData = await Promise.all(tabDataPromises);
console.log('✅ Данные обновлены:', apiTabData.length, 'категорий');
setTabData(apiTabData as PartsIndexTabData[]);
setActiveTabIndex(0);
}
} catch (error) {
console.error('Ошибка загрузки данных Parts Index:', error);
} finally {
setLoading(false);
} }
} }
}; } catch (error) {
console.error('Ошибка загрузки групп для каталога:', tab.catalogId, error);
} finally {
setLoadingGroups(prev => {
const newSet = new Set(prev);
newSet.delete(tabIndex);
return newSet;
});
}
};
loadData(); // Обработчик наведения на таб - загружаем группы
}, []); const handleTabHover = (tabIndex: number) => {
loadGroupsForTab(tabIndex);
};
// Обработчик клика на таб
const handleTabClick = (tabIndex: number) => {
setActiveTabIndex(tabIndex);
// Загружаем группы если еще не загружены
loadGroupsForTab(tabIndex);
};
// Обработка клика по категории для перехода в каталог // Обработка клика по категории для перехода в каталог
const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => { const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => {
@ -184,7 +261,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
query: { query: {
partsIndexCatalog: catalogId, partsIndexCatalog: catalogId,
categoryName: encodeURIComponent(categoryName), categoryName: encodeURIComponent(categoryName),
...(entityId && { entityId }) ...(entityId && { partsIndexCategory: entityId })
} }
}); });
}; };
@ -294,6 +371,12 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
className="mobile-subcategory" className="mobile-subcategory"
key={cat.catalogId} key={cat.catalogId}
onClick={() => { onClick={() => {
// Загружаем группы для категории если нужно
const catIndex = tabData.findIndex(tab => tab.catalogId === cat.catalogId);
if (catIndex !== -1) {
loadGroupsForTab(catIndex);
}
const categoryWithData = { const categoryWithData = {
...cat, ...cat,
catalogId: cat.catalogId, catalogId: cat.catalogId,
@ -304,6 +387,9 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
{cat.label} {cat.label}
{loadingGroups.has(tabData.findIndex(tab => tab.catalogId === cat.catalogId)) && (
<span className="text-xs text-gray-500 ml-2">(загрузка...)</span>
)}
</div> </div>
))} ))}
</div> </div>
@ -367,9 +453,8 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
href="#" href="#"
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`} className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
key={tab.catalogId} key={tab.catalogId}
onClick={() => { onClick={() => handleTabClick(idx)}
setActiveTabIndex(idx); onMouseEnter={() => handleTabHover(idx)}
}}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<div className="div-block-29"> <div className="div-block-29">
@ -388,6 +473,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
<h3 className="heading-16"> <h3 className="heading-16">
{currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading} {currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading}
{loading && <span className="text-sm text-gray-500 ml-2">(обновление...)</span>} {loading && <span className="text-sm text-gray-500 ml-2">(обновление...)</span>}
{loadingGroups.has(activeTabIndex) && <span className="text-sm text-gray-500 ml-2">(загрузка групп...)</span>}
</h3> </h3>
<div className="w-layout-hflex flex-block-92"> <div className="w-layout-hflex flex-block-92">
<div className="w-layout-vflex flex-block-91"> <div className="w-layout-vflex flex-block-91">

View File

@ -373,6 +373,8 @@ export const CREATE_PAYMENT = gql`
} }
` `
export const GET_ORDERS = gql` export const GET_ORDERS = gql`
query GetOrders($clientId: String, $status: OrderStatus, $search: String, $limit: Int, $offset: Int) { query GetOrders($clientId: String, $status: OrderStatus, $search: String, $limit: Int, $offset: Int) {
orders(clientId: $clientId, status: $status, search: $search, limit: $limit, offset: $offset) { orders(clientId: $clientId, status: $status, search: $search, limit: $limit, offset: $offset) {
@ -1367,6 +1369,23 @@ export const GET_PARTSINDEX_CATEGORIES = gql`
} }
`; `;
// Навигационные категории с иконками
export const GET_NAVIGATION_CATEGORIES = gql`
query GetNavigationCategories {
navigationCategories {
id
partsIndexCatalogId
partsIndexGroupId
name
catalogName
groupName
icon
sortOrder
isHidden
}
}
`;
// Новый запрос для получения товаров каталога PartsIndex // Новый запрос для получения товаров каталога PartsIndex
export const GET_PARTSINDEX_CATALOG_ENTITIES = gql` export const GET_PARTSINDEX_CATALOG_ENTITIES = gql`
query GetPartsIndexCatalogEntities( query GetPartsIndexCatalogEntities(

View File

@ -138,7 +138,7 @@ export default function Catalog() {
limit: ITEMS_PER_PAGE, limit: ITEMS_PER_PAGE,
page: partsIndexPage, page: partsIndexPage,
q: searchQuery || undefined, q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined params: undefined // Будем обновлять через refetch
}, },
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-and-network' fetchPolicy: 'cache-and-network'
@ -146,7 +146,7 @@ export default function Catalog() {
); );
// Загружаем параметры фильтрации для PartsIndex // Загружаем параметры фильтрации для PartsIndex
const { data: paramsData, loading: paramsLoading, error: paramsError } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>( const { data: paramsData, loading: paramsLoading, error: paramsError, refetch: refetchParams } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
GET_PARTSINDEX_CATALOG_PARAMS, GET_PARTSINDEX_CATALOG_PARAMS,
{ {
variables: { variables: {
@ -154,7 +154,7 @@ export default function Catalog() {
groupId: groupId as string, groupId: groupId as string,
lang: 'ru', lang: 'ru',
q: searchQuery || undefined, q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined params: undefined // Будем обновлять через refetch
}, },
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-first' fetchPolicy: 'cache-first'
@ -215,18 +215,44 @@ export default function Catalog() {
} }
}, [entitiesData]); }, [entitiesData]);
// Преобразование выбранных фильтров в формат PartsIndex API
const convertFiltersToPartsIndexParams = useCallback((): Record<string, any> => {
if (!paramsData?.partsIndexCatalogParams?.list || Object.keys(selectedFilters).length === 0) {
return {};
}
const apiParams: Record<string, any> = {};
paramsData.partsIndexCatalogParams.list.forEach((param: any) => {
const selectedValues = selectedFilters[param.name];
if (selectedValues && selectedValues.length > 0) {
// Находим соответствующие значения из API данных
const matchingValues = param.values.filter((value: any) =>
selectedValues.includes(value.title || value.value)
);
if (matchingValues.length > 0) {
// Используем ID параметра из API и значения
apiParams[param.id] = matchingValues.map((v: any) => v.value);
}
}
});
return apiParams;
}, [paramsData, selectedFilters]);
// Генерация фильтров для PartsIndex на основе параметров API // Генерация фильтров для PartsIndex на основе параметров API
const generatePartsIndexFilters = useCallback((): FilterConfig[] => { const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
if (!paramsData?.partsIndexCatalogParams?.list) { if (!paramsData?.partsIndexCatalogParams?.list) {
return []; return [];
} }
return paramsData.partsIndexCatalogParams.list.map(param => { return paramsData.partsIndexCatalogParams.list.map((param: any) => {
if (param.type === 'range') { if (param.type === 'range') {
// Для range фильтров ищем min и max значения // Для range фильтров ищем min и max значения
const numericValues = param.values const numericValues = param.values
.map(v => parseFloat(v.value)) .map((v: any) => parseFloat(v.value))
.filter(v => !isNaN(v)); .filter((v: number) => !isNaN(v));
const min = numericValues.length > 0 ? Math.min(...numericValues) : 0; const min = numericValues.length > 0 ? Math.min(...numericValues) : 0;
const max = numericValues.length > 0 ? Math.max(...numericValues) : 100; const max = numericValues.length > 0 ? Math.max(...numericValues) : 100;
@ -243,8 +269,8 @@ export default function Catalog() {
type: 'dropdown' as const, type: 'dropdown' as const,
title: param.name, title: param.name,
options: param.values options: param.values
.filter(value => value.available) // Показываем только доступные .filter((value: any) => value.available) // Показываем только доступные
.map(value => value.title || value.value), .map((value: any) => value.title || value.value),
multi: true, multi: true,
showAll: true showAll: true
}; };
@ -252,6 +278,8 @@ export default function Catalog() {
}); });
}, [paramsData]); }, [paramsData]);
useEffect(() => { useEffect(() => {
if (isPartsIndexMode) { if (isPartsIndexMode) {
// Для PartsIndex генерируем фильтры на основе параметров API // Для PartsIndex генерируем фильтры на основе параметров API
@ -426,16 +454,38 @@ export default function Catalog() {
// При изменении поиска или фильтров сбрасываем пагинацию // При изменении поиска или фильтров сбрасываем пагинацию
setShowEmptyState(false); setShowEmptyState(false);
// Если изменился поисковый запрос, нужно перезагрузить данные с сервера // Если изменился поисковый запрос или фильтры, нужно перезагрузить данные с сервера
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) { if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию'); console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
setPartsIndexPage(1); setPartsIndexPage(1);
setHasMoreEntities(true); setHasMoreEntities(true);
// refetch будет автоматически вызван при изменении partsIndexPage
// Перезагружаем данные с новыми параметрами фильтрации
const apiParams = convertFiltersToPartsIndexParams();
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
// Также обновляем параметры фильтрации
refetchParams({
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
q: searchQuery || undefined,
params: paramsString
});
refetchEntities({
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
limit: ITEMS_PER_PAGE,
page: 1,
q: searchQuery || undefined,
params: paramsString
});
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters)]); }, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), refetchEntities, refetchParams, convertFiltersToPartsIndexParams]);
// Управляем показом пустого состояния с задержкой // Управляем показом пустого состояния с задержкой
useEffect(() => { useEffect(() => {

View File

@ -377,4 +377,23 @@ button,
.tooltip-title { .tooltip-title {
font-size: 15px; font-size: 15px;
} }
}
/* Стили для навигационных иконок */
.navigation-icon-img {
display: block !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
z-index: 9999 !important;
max-width: 21px !important;
max-height: 20px !important;
width: 21px !important;
height: 20px !important;
object-fit: contain !important;
opacity: 1 !important;
visibility: visible !important;
border: 2px solid red !important; /* Временно для отладки */
background-color: yellow !important; /* Временно для отладки */
pointer-events: none !important; /* Чтобы не блокировать клики */
} }

12
src/types/index.ts Normal file
View File

@ -0,0 +1,12 @@
// Навигационные категории
export interface NavigationCategory {
id: string
partsIndexCatalogId: string
partsIndexGroupId: string | null
name: string
catalogName: string
groupName: string | null
icon: string | null
sortOrder: number
isHidden: boolean
}