feat: integrate yandex metrika ecommerce

This commit is contained in:
2025-10-26 18:22:19 +03:00
parent 4e59c4e788
commit 2f74aba35f
18 changed files with 240 additions and 31 deletions

View File

@@ -7,11 +7,11 @@
class="products-grid grid grid-cols-2 gap-x-5 gap-y-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8"
>
<RouterLink
v-for="product in products"
v-for="(product, index) in products"
:key="product.id"
class="product-grid-card group"
:to="`/product/${product.id}`"
@click="haptic"
@click="productClick(product, index)"
>
<ProductImageSwiper :images="product.images"/>
<h3 class="product-title mt-4 text-sm">{{ product.name }}</h3>
@@ -54,7 +54,9 @@ import ProductImageSwiper from "@/components/ProductImageSwiper.vue";
import {useSettingsStore} from "@/stores/SettingsStore.js";
import {ref} from "vue";
import {useIntersectionObserver} from '@vueuse/core';
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
const yaMetrika = useYaMetrikaStore();
const settings = useSettingsStore();
const bottom = ref(null);
@@ -87,8 +89,26 @@ const props = defineProps({
}
});
function haptic() {
function productClick(product, index) {
window.Telegram.WebApp.HapticFeedback.selectionChanged();
yaMetrika.dataLayerPush({
"ecommerce": {
"currencyCode": settings.currency_code,
"click": {
"products": [
{
"id": product.id,
"name": product.name,
"price": product.final_price_numeric,
"brand": product.manufacturer_name,
"category": product.category_name,
"list": "Главная страница",
"position": index,
}
]
}
}
});
}
useIntersectionObserver(

View File

@@ -1,5 +1,6 @@
export const YA_METRIKA_GOAL = {
ADD_TO_CART: 'add_to_cart',
PRODUCT_OPEN_EXTERNAL: 'product_open_external',
CREATE_ORDER: 'create_order',
ORDER_CREATED_SUCCESS: 'order_created_success',
VIEW_PRODUCT: 'view_product',

View File

@@ -1,6 +1,8 @@
import {defineStore} from "pinia";
import {isNotEmpty} from "@/helpers.js";
import {addToCart, cartEditItem, cartRemoveItem, getCart, setCoupon, setVoucher} from "@/utils/ftch.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";
export const useCartStore = defineStore('cart', {
state: () => ({
@@ -79,12 +81,27 @@ export const useCartStore = defineStore('cart', {
}
},
async removeItem(rowId) {
async removeItem(cartItem, rowId, index = 0) {
try {
this.isLoading = true;
const formData = new FormData();
formData.append('key', rowId);
await cartRemoveItem(formData);
useYaMetrikaStore().dataLayerPush({
"ecommerce": {
"currencyCode": useSettingsStore().currency_code,
"remove": {
"products": [
{
"id": cartItem.product_id,
"name": cartItem.name,
"quantity": cartItem.quantity,
"position": index
}
]
}
}
});
await this.getProducts();
} catch (error) {
console.error(error);

View File

@@ -2,6 +2,9 @@ import {defineStore} from "pinia";
import {isNotEmpty} from "@/helpers.js";
import {storeOrder} from "@/utils/ftch.js";
import {useCartStore} from "@/stores/CartStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";
export const useCheckoutStore = defineStore('checkout', {
state: () => ({
@@ -56,6 +59,37 @@ export const useCheckoutStore = defineStore('checkout', {
const response = await storeOrder(this.customer);
this.order = response.data;
if (! this.order.id) {
console.debug(response.data);
throw new Error('Ошибка создания заказа.');
}
const yaMetrika = useYaMetrikaStore();
yaMetrika.reachGoal(YA_METRIKA_GOAL.ORDER_CREATED_SUCCESS, {
price: this.order?.final_total_numeric,
currency: this.order?.currency,
});
yaMetrika.dataLayerPush({
"ecommerce": {
"currencyCode": useSettingsStore().currency_code,
"purchase": {
"actionField": {
"id": this.order.id,
'revenue': this.order?.final_total_numeric,
},
"products": this.order.products ? this.order.products.map((product, index) => {
return {
id: product.product_id,
name: product.name,
price: product.total_numeric,
position: index,
quantity: product.quantif,
};
}) : [],
}
}
});
await window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
await useCartStore().getProducts();
} catch (error) {

View File

@@ -17,6 +17,7 @@ export const useSettingsStore = defineStore('settings', {
ya_metrika_enabled: false,
feature_coupons: false,
feature_vouchers: false,
currency_code: null,
theme: {
light: 'light', dark: 'dark', variables: {
'--product_list_title_max_lines': 2,
@@ -44,6 +45,7 @@ export const useSettingsStore = defineStore('settings', {
this.store_enabled = settings.store_enabled;
this.feature_coupons = settings.feature_coupons;
this.feature_vouchers = settings.feature_vouchers;
this.currency_code = settings.currency_code;
}
}
});

View File

@@ -11,8 +11,7 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
actions: {
pushHit(url, params = {}) {
const settings = useSettingsStore();
if (!settings.ya_metrika_enabled) {
if (!useSettingsStore().ya_metrika_enabled) {
console.debug('[ym] Yandex Metrika disabled in settings.');
return;
}
@@ -39,8 +38,7 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
},
reachGoal(target, params = {}) {
const settings = useSettingsStore();
if (!settings.ya_metrika_enabled) {
if (!useSettingsStore().ya_metrika_enabled) {
console.debug('[ym] Yandex Metrika disabled in settings.');
return;
}
@@ -61,6 +59,11 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
},
initUserParams() {
if (!useSettingsStore().ya_metrika_enabled) {
console.debug('[ym] Yandex Metrika disabled in settings.');
return;
}
if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) {
let tgID = null;
@@ -95,6 +98,9 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
window.ym(window.YA_METRIKA_ID, item.event, item.payload.url, item.payload.params);
} else if (item.event === 'reachGoal') {
window.ym(window.YA_METRIKA_ID, item.event, item.payload.target, item.payload.params);
} else if (item.event === 'dataLayer') {
console.debug('[ym] queue dataLayer push: ', item.payload);
window.dataLayer.push(item.payload);
} else {
console.error('[ym] Unsupported queue event: ', item.event);
}
@@ -102,5 +108,23 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
console.debug('[ym] Queue processing complete. Size: ', this.queue.length);
},
dataLayerPush(object) {
if (!useSettingsStore().ya_metrika_enabled) {
console.debug('[ym] Yandex Metrika disabled in settings.');
return;
}
if (Array.isArray(window.dataLayer)) {
console.debug('[ym] dataLayer push: ', object);
window.dataLayer.push(object);
} else {
console.debug('[ym] dataLayer inaccessible. Put to queue');
this.queue.push({
event: 'dataLayer',
payload: object,
});
}
}
},
});

View File

@@ -24,6 +24,7 @@ export function injectYaMetrika() {
console.debug('[Init] Detected Yandex.Metrika ID:', window.YA_METRIKA_ID);
const yaMetrika = useYaMetrikaStore();
yaMetrika.initUserParams();
window.dataLayer = window.dataLayer || [];
yaMetrika.processQueue();
}
}

View File

@@ -21,7 +21,7 @@
<div v-if="cart.items.length > 0">
<div
v-for="item in cart.items"
v-for="(item, index) in cart.items"
:key="item.cart_id"
class="card card-border bg-base-100 card-sm mb-3"
:class="item.stock === false ? 'border-error' : ''"
@@ -62,7 +62,7 @@
v-model="item.quantity"
@update:modelValue="cart.setQuantity(item.cart_id, $event)"
/>
<button class="btn btn-error" @click="removeItem(item.cart_id)" :disabled="cart.isLoading">
<button class="btn btn-error" @click="removeItem(item, item.cart_id, index)" :disabled="cart.isLoading">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
@@ -162,8 +162,8 @@ const lastTotal = computed(() => {
return cart.totals.at(-1) ?? null;
});
function removeItem(cartId) {
cart.removeItem(cartId);
function removeItem(cartItem, cartId, index) {
cart.removeItem(cartItem, cartId, index);
window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
}

View File

@@ -74,6 +74,7 @@ import {computed, onMounted, ref} from "vue";
import {IMaskComponent} from "vue-imask";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";
const checkout = useCheckoutStore();
const yaMetrika = useYaMetrikaStore();
@@ -92,11 +93,9 @@ async function onCreateBtnClick() {
price: checkout.order?.final_total_numeric,
currency: checkout.order?.currency,
});
await checkout.makeOrder();
yaMetrika.reachGoal(YA_METRIKA_GOAL.ORDER_CREATED_SUCCESS, {
price: checkout.order?.final_total_numeric,
currency: checkout.order?.currency,
});
router.push({name: 'order_created'});
} catch {
error.value = 'Невозможно создать заказ.';

View File

@@ -39,6 +39,7 @@ import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import Banner from "@/components/Banner.vue";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";
defineOptions({
name: 'Home'
@@ -48,12 +49,14 @@ const router = useRouter();
const filtersStore = useProductFiltersStore();
const yaMetrika = useYaMetrikaStore();
const haptic = window.Telegram.WebApp.HapticFeedback;
const settings = useSettingsStore();
const products = ref([]);
const hasMore = ref(false);
const isLoading = ref(false);
const isLoadingMore = ref(false);
const page = ref(1);
const perPage = 20;
function showFilters() {
haptic.impactOccurred('soft');
@@ -67,11 +70,31 @@ async function fetchProducts() {
console.debug('Home: Fetch products from server using filters: ', toRaw(filtersStore.applied));
const response = await ftch('products', null, toRaw({
page: page.value,
perPage: perPage,
filters: filtersStore.applied,
}));
products.value = response.data;
hasMore.value = response.meta.hasMore;
console.debug('Home: Products for main page loaded.');
yaMetrika.dataLayerPush({
ecommerce: {
currencyCode: settings.currency_code,
impressions: products.value.map((product, index) => {
return {
id: product.id,
name: product.name,
price: product.final_price_numeric,
brand: product.manufacturer_name,
category: product.category_name,
list: 'Главная страница',
position: index,
discount: product.price_numeric - product.final_price_numeric,
quantity: product.product_quantity,
};
}),
},
});
} catch (error) {
console.error(error);
} finally {

View File

@@ -224,6 +224,25 @@ async function onCartBtnClick() {
price: product.value.final_price_numeric,
currency: product.value.currency,
});
yaMetrika.dataLayerPush({
"ecommerce": {
"currencyCode": settings.currency_code,
"add": {
"products": [
{
"id": product.value?.id,
"name": product.value?.name,
"price": product.value?.final_price_numeric,
"brand": product.value?.manufacturer,
"category": product.value?.category?.name,
"quantity": 1,
"list": "Выдача категории",
"position": 2
}
]
}
}
});
} else {
window.Telegram.WebApp.HapticFeedback.selectionChanged();
await router.push({'name': 'cart'});
@@ -240,6 +259,11 @@ function openProductInMarketplace() {
return;
}
yaMetrika.reachGoal(YA_METRIKA_GOAL.PRODUCT_OPEN_EXTERNAL, {
price: product.value?.final_price_numeric,
currency: product.value?.currency,
});
window.Telegram.WebApp.openLink(product.value.share, {try_instant_view: false});
}
@@ -285,6 +309,23 @@ onMounted(async () => {
price: data.final_price_numeric,
currency: data.currency,
});
yaMetrika.dataLayerPush({
"ecommerce": {
"currencyCode": settings.currency_code,
"detail": {
"products": [
{
"id": data.product_id,
"name": data.name,
"price": data.final_price_numeric,
"brand": data.manufacturer,
"category": data.category?.name,
}
]
}
}
});
} catch (error) {
console.error(error);
} finally {