Разработка компонента избранного на Vue.js для 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Разработка компонента избранного на Vue.js для 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    745
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Разработка компонента избранного на 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 недели

Избранное — не просто функция удобства. Это механизм возврата покупателя: человек добавил товар в вишлист, ушёл, вернулся через неделю и купил. Без избранного этот сценарий заканчивается на шаге «ушёл».