feat(search): improvement search cache (#44)

This commit is contained in:
2026-01-05 15:35:18 +03:00
committed by Nikita Kiselev
parent 089b686722
commit 8a9bac8221
4 changed files with 106 additions and 21 deletions

View File

@@ -17,6 +17,13 @@ export const useSearchStore = defineStore('search', {
isLoadingMore: false,
isSearchPerformed: false,
hasMore: false,
// Placeholder товары для пустого состояния поиска
placeholderProducts: {
data: [],
total: 0,
},
isLoadingPlaceholder: false,
}),
actions: {
@@ -91,6 +98,35 @@ export const useSearchStore = defineStore('search', {
this.isSearchPerformed = true;
}
},
async loadSearchPlaceholder() {
// Если данные уже есть в store, возвращаем их
if (this.placeholderProducts.data.length > 0) {
return {
data: this.placeholderProducts.data,
meta: {
total: this.placeholderProducts.total,
},
};
}
try {
this.isLoadingPlaceholder = true;
// Иначе загружаем с сервера
const response = await ftch('productsSearchPlaceholder');
this.placeholderProducts.data = response.data.slice(0, 3);
this.placeholderProducts.total = response?.meta?.total || 0;
return {
data: this.placeholderProducts.data,
meta: {
total: this.placeholderProducts.total,
},
};
} finally {
this.isLoadingPlaceholder = false;
}
},
},
});

View File

@@ -79,25 +79,38 @@
class="flex flex-col items-center justify-center text-center py-16 px-10"
>
<div class="avatar-group -space-x-6 mb-4">
<template v-for="product in preloadedProducts" :key="product.id">
<div v-if="product.image" class="avatar">
<div class="w-12">
<img :src="product.image" :alt="product.name"/>
</div>
<!-- Skeleton при загрузке -->
<template v-if="searchStore.isLoadingPlaceholder">
<div v-for="n in 3" :key="n" class="avatar">
<div class="w-12 skeleton rounded-full"></div>
</div>
<div v-else class="avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-12 rounded-full">
<span class="text-3xl">{{ product.name.charAt(0).toUpperCase() }}</span>
</div>
<div class="avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-12 skeleton"></div>
</div>
</template>
<div class="avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-12">
<span>{{ renderSmartNumber(productsTotal) }}</span>
<!-- Товары после загрузки -->
<template v-else>
<template v-for="product in preloadedProducts" :key="product.id">
<div v-if="product.image" class="avatar">
<div class="w-12">
<img :src="product.image" :alt="product.name"/>
</div>
</div>
<div v-else class="avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-12 rounded-full">
<span class="text-3xl">{{ product.name.charAt(0).toUpperCase() }}</span>
</div>
</div>
</template>
<div class="avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-12">
<span>{{ renderSmartNumber(productsTotal) }}</span>
</div>
</div>
</div>
</template>
</div>
<h2 class="text-xl font-semibold mb-2">Поиск товаров</h2>
<p class="text-sm mb-4">Введите запрос, чтобы отобразить подходящие товары.</p>
@@ -185,8 +198,8 @@ const handleHideKeyboardClick = () => {
}
};
const preloadedProducts = ref([]);
const productsTotal = ref(0);
const preloadedProducts = computed(() => searchStore.placeholderProducts.data);
const productsTotal = computed(() => searchStore.placeholderProducts.total);
const searchWrapperStyle = computed(() => {
const safeTop = getComputedStyle(document.documentElement)
@@ -208,8 +221,6 @@ onMounted(async () => {
});
yaMetrika.reachGoal(YA_METRIKA_GOAL.VIEW_SEARCH);
const response = await searchStore.fetchProducts('', 1, 3);
productsTotal.value = response?.meta?.total || 0;
preloadedProducts.value = response.data.splice(0, 3);
await searchStore.loadSearchPlaceholder();
});
</script>

View File

@@ -7,6 +7,7 @@ namespace App\Handlers;
use App\Services\ProductsService;
use App\Services\SettingsService;
use Exception;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Exceptions\EntityNotFoundException;
use Openguru\OpenCartFramework\Http\Request;
use Psr\Log\LoggerInterface;
@@ -19,12 +20,18 @@ class ProductsHandler
private SettingsService $settings;
private ProductsService $productsService;
private LoggerInterface $logger;
private CacheInterface $cache;
public function __construct(SettingsService $settings, ProductsService $productsService, LoggerInterface $logger)
{
public function __construct(
SettingsService $settings,
ProductsService $productsService,
LoggerInterface $logger,
CacheInterface $cache
) {
$this->settings = $settings;
$this->productsService = $productsService;
$this->logger = $logger;
$this->cache = $cache;
}
public function index(Request $request): JsonResponse
@@ -85,4 +92,34 @@ class ProductsHandler
'data' => $images,
]);
}
public function getSearchPlaceholder(Request $request): JsonResponse
{
$storeId = $this->settings->get('store.oc_store_id', 0);
$languageId = $this->settings->config()->getApp()->getLanguageId();
$cacheKey = "products.search_placeholder.{$storeId}.{$languageId}";
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return new JsonResponse($cached);
}
$response = $this->productsService->getProductsResponse(
[
'page' => 1,
'perPage' => 3,
'search' => '',
'filters' => [],
'maxPages' => 1,
],
$languageId,
$storeId,
);
// Кешируем на 24 часа
$this->cache->set($cacheKey, $response, 60 * 60 * 24);
return new JsonResponse($response);
}
}

View File

@@ -28,6 +28,7 @@ return [
'processBlock' => [BlocksHandler::class, 'processBlock'],
'product_show' => [ProductsHandler::class, 'show'],
'products' => [ProductsHandler::class, 'index'],
'productsSearchPlaceholder' => [ProductsHandler::class, 'getSearchPlaceholder'],
'saveTelegramCustomer' => [TelegramCustomerHandler::class, 'saveOrUpdate'],
'getCurrentCustomer' => [TelegramCustomerHandler::class, 'getCurrent'],
'settings' => [SettingsHandler::class, 'index'],