feat(search): improvement search cache (#44)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user