Розробка компонента вибраного на Vue.js для 1С-Бітрікс
Функціонал вибраного (wishlist) у Бітрікс нерідко реалізується як окремий список порівняння або як кастомний кошик з особливим статусом. Обидва підходи — милиці. Vue.js-компонент вибраного будується правильно: окрема сутність, персистентне зберігання для авторизованих, guest-режим через localStorage, миттєва реакція UI без перезавантаження сторінки.
Що важливо спроектувати до написання коду
Для кого зберігається вибране? Якщо для неавторизованих — тільки localStorage. Якщо хочемо, щоб вибране зберігалося при логіні — потрібна база даних і механізм мерджу: при авторизації гостьове вибране з localStorage об'єднується з серверним.
Скільки списків? Простий варіант — один список на користувача. Просунутий — кілька іменованих вішлістів («Подарунки на день народження», «Хочу навесні»). Другий варіант значно складніший — з окремим інтерфейсом управління списками.
Що робити при додаванні товару, якого немає в наявності? Сповіщати, коли з'явиться — це окремий функціонал «Підписка на наявність», пов'язаний з вибраним.
Структура бази даних
CREATE TABLE b_user_wishlist (
ID SERIAL PRIMARY KEY,
USER_ID INT NOT NULL REFERENCES b_user(ID),
LIST_NAME VARCHAR(255) DEFAULT 'default',
DATE_CREATE TIMESTAMP DEFAULT NOW()
);
CREATE TABLE b_wishlist_items (
ID SERIAL PRIMARY KEY,
LIST_ID INT NOT NULL REFERENCES b_user_wishlist(ID),
PRODUCT_ID INT NOT NULL,
DATE_ADD TIMESTAMP DEFAULT NOW(),
UNIQUE (LIST_ID, PRODUCT_ID)
);
Для простого варіанту (один список) b_user_wishlist можна не створювати — достатньо таблиці з USER_ID і PRODUCT_ID.
Pinia store для вибраного
// stores/wishlistStore.ts
export const useWishlistStore = defineStore('wishlist', () => {
const items = ref<number[]>([])
const loading = ref<Set<number>>(new Set())
// Завантаження при ініціалізації
async function init() {
if (isLoggedIn()) {
const data = await api.get('/local/api/wishlist/')
items.value = data.map((i: any) => i.product_id)
} else {
const stored = localStorage.getItem('wishlist')
items.value = stored ? JSON.parse(stored) : []
}
}
async function toggle(productId: number) {
if (loading.value.has(productId)) return
loading.value.add(productId)
const wasAdded = items.value.includes(productId)
// Оптимістичне оновлення
if (wasAdded) {
items.value = items.value.filter(id => id !== productId)
} else {
items.value.push(productId)
}
// Персист
if (isLoggedIn()) {
try {
await api.post('/local/api/wishlist/' + (wasAdded ? 'remove' : 'add') + '/', { product_id: productId })
} catch (e) {
// Відкат при помилці
if (wasAdded) items.value.push(productId)
else items.value = items.value.filter(id => id !== productId)
}
} else {
localStorage.setItem('wishlist', JSON.stringify(items.value))
}
loading.value.delete(productId)
}
const isInWishlist = (id: number) => items.value.includes(id)
const count = computed(() => items.value.length)
return { items, loading, init, toggle, isInWishlist, count }
})
Оптимістичне оновлення (Optimistic UI) — спочатку змінюємо UI, потім відправляємо запит. При помилці відкочуємо. Користувач бачить миттєву реакцію.
Кнопка вибраного на картці товару
<!-- WishlistButton.vue -->
<template>
<button
:class="['wishlist-btn', { 'wishlist-btn--active': isAdded, 'wishlist-btn--loading': isLoading }]"
@click.prevent="handleToggle"
:aria-label="isAdded ? 'Прибрати з вибраного' : 'У вибране'"
>
<HeartIcon :filled="isAdded" />
</button>
</template>
<script setup lang="ts">
const props = defineProps<{ productId: number }>()
const store = useWishlistStore()
const isAdded = computed(() => store.isInWishlist(props.productId))
const isLoading = computed(() => store.loading.has(props.productId))
async function handleToggle() {
await store.toggle(props.productId)
// Показати toast: "Додано до вибраного" / "Видалено"
showToast(isAdded.value ? 'Додано до вибраного' : 'Видалено з вибраного')
}
</script>
Toast-сповіщення — ненав'язливі повідомлення в куті екрана, які зникають через 3 секунди. Реалізуються через окремий Toast-компонент або невелику бібліотеку (vue-toastification, vuedraggable).
Сторінка вибраного
<!-- WishlistPage.vue -->
<template>
<div class="wishlist-page">
<h1>Вибране <span class="count">{{ products.length }}</span></h1>
<div v-if="!products.length" class="wishlist-empty">
<p>Список вибраного порожній</p>
<RouterLink to="/catalog/">Перейти до каталогу</RouterLink>
</div>
<div v-else class="wishlist-grid">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
:show-wishlist-btn="true"
/>
</div>
<!-- Додати всі в кошик -->
<button v-if="availableProducts.length" @click="addAllToCart">
Додати в кошик доступні ({{ availableProducts.length }})
</button>
</div>
</template>
<script setup lang="ts">
const store = useWishlistStore()
const products = ref([])
onMounted(async () => {
if (!store.items.length) return
const res = await fetch(`/local/api/wishlist/products/?ids=${store.items.join(',')}`)
products.value = await res.json()
})
const availableProducts = computed(() => products.value.filter(p => p.available))
async function addAllToCart() {
const cartStore = useCartStore()
for (const product of availableProducts.value) {
await cartStore.add(product.id, 1)
}
showToast(`Додано ${availableProducts.value.length} товарів у кошик`)
}
</script>
Мердж guest → user при авторизації
При логіні користувача викликається серверний метод мерджу:
// WishlistController.php
public function mergeGuestAction(array $guestIds): array {
global $USER;
$userId = (int) $USER->GetID();
foreach ($guestIds as $productId) {
// Додаємо тільки якщо ще немає
WishlistItemTable::merge(['USER_ID' => $userId, 'PRODUCT_ID' => (int)$productId]);
}
// Повертаємо повний актуальний список
return WishlistItemTable::getProductIdsByUser($userId);
}
У JS: при події успішного логіну (або при ініціалізації авторизованого користувача):
async function onUserLogin() {
const guestIds = JSON.parse(localStorage.getItem('wishlist') ?? '[]')
if (guestIds.length) {
const merged = await api.post('/local/api/wishlist/merge/', { ids: guestIds })
items.value = merged
localStorage.removeItem('wishlist')
} else {
await init() // Завантажуємо серверний список
}
}
Підписка на наявність з вибраного
Розширення: якщо товар з вибраного з'явився на складі, користувач отримує сповіщення. Реалізація:
CREATE TABLE b_availability_subscriptions (
USER_ID INT NOT NULL,
PRODUCT_ID INT NOT NULL,
EMAIL VARCHAR(255),
DATE_ADD TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (USER_ID, PRODUCT_ID)
);
Агент Бітрікс перевіряє таблицю раз на годину: якщо товар знову з'явився в наявності (QUANTITY > 0), відправляє листа через CEvent::Send() і видаляє підписку.
Аналітика вибраного
Дані вибраного — цінний сигнал про попит. Товари в топі вибраного з нульовими залишками — кандидати на пріоритетне поповнення. Аналітичний дашборд: топ-50 товарів у вибраному, конверсія «з вибраного в покупку» (через JOIN з b_sale_basket).
Терміни
| Варіант | Що входить | Термін |
|---|---|---|
| Guest-only wishlist | localStorage + кнопки + сторінка | 4–7 днів |
| Авторизований з персистом | + БД, API, мердж | 1–2 тижні |
| + Мульти-списки, підписка | + управління списками, сповіщення | +1–2 тижні |
Вибране — не просто функція зручності. Це механізм повернення покупця: людина додала товар у вішліст, пішла, повернулася через тиждень і купила. Без вибраного цей сценарій закінчується на кроці «пішла».







