From cd060610fe991c7c6d0db81a24bfa2b062192d20 Mon Sep 17 00:00:00 2001 From: Nikita Kiselev Date: Sat, 6 Dec 2025 15:46:37 +0300 Subject: [PATCH] feat(products-feed): replace fixed image dimensions with aspect ratio selection - Added image aspect ratio selection (1:1, 4:5, 3:4, 2:3) to ProductsFeed block configuration in Admin panel - Removed manual width/height input fields - Updated ProductsFeed block in SPA to send aspect ratio parameter instead of dimensions - Implemented backend logic to calculate image height based on selected aspect ratio and base width (300px) - Updated default configuration for products_feed block - Added descriptive help text for each aspect ratio option in the dropdown --- .../Blocks/ProductsFeedBlock.vue | 4 ++ .../Forms/ProductsFeedForm.vue | 48 ++++++++++++++++++- .../MainPageConfigurator/availableBlocks.js | 1 + .../MainPage/Blocks/ProductsFeedBlock.vue | 17 ++++--- frontend/spa/src/components/ProductsList.vue | 3 +- frontend/spa/src/stores/SettingsStore.js | 2 +- .../upload/oc_telegram_shop/configs/app.php | 1 + .../src/Handlers/ProductsHandler.php | 9 +++- .../src/Services/BlocksService.php | 18 +++++++ .../src/Services/ProductsService.php | 13 +++-- 10 files changed, 99 insertions(+), 17 deletions(-) diff --git a/frontend/admin/src/components/MainPageConfigurator/Blocks/ProductsFeedBlock.vue b/frontend/admin/src/components/MainPageConfigurator/Blocks/ProductsFeedBlock.vue index 05cf9aa..d781402 100644 --- a/frontend/admin/src/components/MainPageConfigurator/Blocks/ProductsFeedBlock.vue +++ b/frontend/admin/src/components/MainPageConfigurator/Blocks/ProductsFeedBlock.vue @@ -13,6 +13,10 @@ Максимальное кол-во страниц: {{ value.data.max_page_count }} +
+ Соотношение сторон: + {{ value.data.image_aspect_ratio || '1:1' }} +
diff --git a/frontend/admin/src/components/MainPageConfigurator/Forms/ProductsFeedForm.vue b/frontend/admin/src/components/MainPageConfigurator/Forms/ProductsFeedForm.vue index 6073915..3fbe455 100644 --- a/frontend/admin/src/components/MainPageConfigurator/Forms/ProductsFeedForm.vue +++ b/frontend/admin/src/components/MainPageConfigurator/Forms/ProductsFeedForm.vue @@ -26,6 +26,29 @@ Ограничение страниц снижает нагрузку на сервер. + + + + + @@ -35,14 +58,31 @@ import {computed, defineExpose, onMounted, ref} from "vue"; import {md5} from "js-md5"; import BaseForm from "@/components/MainPageConfigurator/Forms/BaseForm.vue"; -import {InputNumber} from "primevue"; +import {InputNumber, Dropdown} from "primevue"; import FormItem from "@/components/MainPageConfigurator/Forms/FormItem.vue"; const draft = ref(null); const model = defineModel(); const emit = defineEmits(['cancel']); -const isChanged = computed(() => md5(JSON.stringify(model.value)) !== md5(JSON.stringify(draft.value))); +const aspectRatioOptions = [ + { label: '1:1', value: '1:1', description: 'Универсально, аксессуары, мелкие товары, удобно для всех товаров — идеально для сетки.' }, + { label: '4:5', value: '4:5', description: 'Одежда, обувь, вертикальные товары, где нужно показать высоту (футболки, платья).' }, + { label: '3:4', value: '3:4', description: 'Одежда, обувь, вертикальные товары, где нужно показать высоту (футболки, платья).' }, + { label: '2:3', value: '2:3', description: 'Цветы, высокие предметы (бутылки, букеты, декоративные элементы).' }, +]; + +const isChanged = computed(() => { + const normalize = (obj) => { + return JSON.stringify(obj, (key, value) => { + if (['max_page_count'].includes(key)) { + return value !== null && value !== undefined && value !== '' ? parseInt(value) : value; + } + return value; + }); + }; + return md5(normalize(model.value)) !== md5(normalize(draft.value)); +}); function onApply() { model.value = JSON.parse(JSON.stringify(draft.value)); @@ -50,6 +90,10 @@ function onApply() { onMounted(() => { draft.value = JSON.parse(JSON.stringify(model.value)); + if (draft.value.data) { + if (draft.value.data.max_page_count) draft.value.data.max_page_count = parseInt(draft.value.data.max_page_count); + if (!draft.value.data.image_aspect_ratio) draft.value.data.image_aspect_ratio = '1:1'; + } }); defineExpose({isChanged}); diff --git a/frontend/admin/src/components/MainPageConfigurator/availableBlocks.js b/frontend/admin/src/components/MainPageConfigurator/availableBlocks.js index 2ec0d99..e62ed17 100644 --- a/frontend/admin/src/components/MainPageConfigurator/availableBlocks.js +++ b/frontend/admin/src/components/MainPageConfigurator/availableBlocks.js @@ -58,6 +58,7 @@ export const blocks = [ goal_name: '', data: { max_page_count: 10, + image_aspect_ratio: '1:1', }, }, { diff --git a/frontend/spa/src/components/MainPage/Blocks/ProductsFeedBlock.vue b/frontend/spa/src/components/MainPage/Blocks/ProductsFeedBlock.vue index e2e3a48..0fc8cb1 100644 --- a/frontend/spa/src/components/MainPage/Blocks/ProductsFeedBlock.vue +++ b/frontend/spa/src/components/MainPage/Blocks/ProductsFeedBlock.vue @@ -33,7 +33,7 @@ const hasMore = ref(false); const isLoading = ref(false); const isLoadingMore = ref(false); const page = ref(1); -const perPage = 20; +const perPage = 10; const props = defineProps({ block: { @@ -45,17 +45,17 @@ const props = defineProps({ async function fetchProducts() { try { isLoading.value = true; - console.debug('Home: Load products for Main Page.'); - console.debug('Home: Fetch products from server using filters: ', toRaw(filtersStore.applied)); + console.debug('[Products Feed]: Start to load products for page 1. Filters: ', toRaw(filtersStore.applied)); const response = await ftch('products', null, toRaw({ - page: page.value, + page: 1, maxPages: props.block.data.max_page_count, perPage: perPage, filters: filtersStore.applied, + image_aspect_ratio: props.block.data.image_aspect_ratio, })); products.value = response.data; hasMore.value = response.meta.hasMore; - console.debug('ProductsFeedBlock: Products for main page loaded.'); + console.debug('[Products Feed]: Products loaded for page 1. Has More: ', hasMore.value); yaMetrika.dataLayerPush({ ecommerce: { @@ -84,18 +84,21 @@ async function fetchProducts() { async function onLoadMore() { try { - console.debug('ProductsFeedBlock: onLoadMore'); + console.debug('[Products Feed]: onLoadMore'); if (isLoading.value === true || isLoadingMore.value === true || hasMore.value === false) return; isLoadingMore.value = true; page.value++; - console.debug('ProductsFeedBlock: Load more for page ', page.value, ' using filters: ', toRaw(filtersStore.applied)); + console.debug('[Products Feed]: Load more for page ', page.value, ' using filters: ', toRaw(filtersStore.applied)); const response = await ftch('products', null, toRaw({ page: page.value, + perPage: perPage, maxPages: props.block.data.max_page_count, filters: filtersStore.applied, + image_aspect_ratio: props.block.data.image_aspect_ratio, })); products.value.push(...response.data); hasMore.value = response.meta.hasMore; + console.debug(`[Products Feed]: Products loaded for page ${page.value}. Has More: `, hasMore.value); } catch (error) { console.error(error); } finally { diff --git a/frontend/spa/src/components/ProductsList.vue b/frontend/spa/src/components/ProductsList.vue index 22302cd..a92a025 100644 --- a/frontend/spa/src/components/ProductsList.vue +++ b/frontend/spa/src/components/ProductsList.vue @@ -131,12 +131,13 @@ function productClick(product, index) { useIntersectionObserver( bottom, ([entry]) => { - console.debug('Check Intersection'); + console.debug('[Product List]: Check Intersection: ', entry?.isIntersecting); if (entry?.isIntersecting === true && props.hasMore === true && props.isLoading === false && props.isLoadingMore === false ) { + console.debug('[Product List]: Send Load More signal'); emits('loadMore'); } }, diff --git a/frontend/spa/src/stores/SettingsStore.js b/frontend/spa/src/stores/SettingsStore.js index 05b1aef..04cc32d 100644 --- a/frontend/spa/src/stores/SettingsStore.js +++ b/frontend/spa/src/stores/SettingsStore.js @@ -29,7 +29,7 @@ export const useSettingsStore = defineStore('settings', { text_order_created_success: 'Заказ успешно оформлен.', }, mainpage_blocks: [], - is_privacy_consented: false, + is_privacy_consented: true, privacy_policy_link: false, }), diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php b/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php index c574692..7bb6529 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php @@ -73,6 +73,7 @@ TEXT, 'goal_name' => '', 'data' => [ 'max_page_count' => 10, + 'image_aspect_ratio' => '1:1', ], ], ], diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php index 50eb7f0..84e19d4 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php @@ -8,11 +8,11 @@ use App\Services\ProductsService; use App\Services\SettingsService; use Exception; use Openguru\OpenCartFramework\Exceptions\EntityNotFoundException; -use Symfony\Component\HttpFoundation\JsonResponse; use Openguru\OpenCartFramework\Http\Request; -use Symfony\Component\HttpFoundation\Response; use Psr\Log\LoggerInterface; use RuntimeException; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; class ProductsHandler { @@ -34,11 +34,16 @@ class ProductsHandler $maxPages = (int) $request->json('maxPages', 10); $search = trim($request->get('search', '')); $filters = $request->json('filters'); + $width = (int) $request->json('width', 300); + $height = (int) $request->json('height', 300); + $languageId = $this->settings->config()->getApp()->getLanguageId(); $response = $this->productsService->getProductsResponse( compact('page', 'perPage', 'search', 'filters', 'maxPages'), $languageId, + $width, + $height, ); return new JsonResponse($response); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/BlocksService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/BlocksService.php index 27138f8..6920b21 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/BlocksService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/BlocksService.php @@ -6,6 +6,7 @@ use Openguru\OpenCartFramework\Cache\CacheInterface; use Openguru\OpenCartFramework\ImageTool\ImageFactory; use Openguru\OpenCartFramework\QueryBuilder\Builder; use Openguru\OpenCartFramework\QueryBuilder\JoinClause; +use Openguru\OpenCartFramework\Support\Arr; use RuntimeException; class BlocksService @@ -17,6 +18,13 @@ class BlocksService 'products_carousel' => [self::class, 'processProductsCarousel'], ]; + private static array $aspectRatiosMap = [ + '1:1' => [400, 400], + '4:5' => [400, 500], + '3:4' => [400, 533], + '2:3' => [400, 600], + ]; + private ImageFactory $image; private CacheInterface $cache; private SettingsService $settings; @@ -113,6 +121,11 @@ class BlocksService private function processProductsFeed(array $block): array { + [$width, $height] = $this->aspectRatioToSize(Arr::get($block, 'data.image_aspect_ratio', '1:1')); + + Arr::set($block, 'data.image_width', $width); + Arr::set($block, 'data.image_height', $height); + return $block; } @@ -147,4 +160,9 @@ class BlocksService return $block; } + + private function aspectRatioToSize($aspectRatio): array + { + return self::$aspectRatiosMap[$aspectRatio] ?? [400, 400]; + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php index 70092bd..b51bb42 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php @@ -53,17 +53,22 @@ class ProductsService /** * @throws ImageNotFoundException */ - public function getProductsResponse(array $params, int $languageId): array - { + public function getProductsResponse( + array $params, + int $languageId, + int $imageWidth = 300, + int $imageHeight = 300 + ): array { $page = $params['page']; $perPage = $params['perPage']; $search = $params['search'] ?? false; $categoryName = ''; - $imageWidth = 300; - $imageHeight = 300; $maxPages = $params['maxPages'] ?? 50; $filters = $params['filters'] ?? []; + $imageWidth = $imageWidth ?: 300; + $imageHeight = $imageHeight ?: 300; + $customerGroupId = $this->settings->config()->getOrders()->getOcCustomerGroupId(); $currency = $this->settings->config()->getStore()->getOcDefaultCurrency();