feat(ya_metrika): WIP yandex metrika

This commit is contained in:
2025-10-26 01:22:29 +03:00
parent 05e7cafd0f
commit d7666f94ba
13 changed files with 135 additions and 17 deletions

View File

@@ -531,7 +531,7 @@ HTML,
'type' => 'select', 'type' => 'select',
'options' => $this->getBannersList(), 'options' => $this->getBannersList(),
'help' => <<<HTML 'help' => <<<HTML
<a href="{$ocBannersLink}" target="_blank">Стандартный OpenCart баннер</a> отображаемый на главной странице магазина. Рекомендуемая максимальная высота изображения для баннера - 200 пикселей. <a href="{$ocBannersLink}" target="_blank">Стандартный OpenCart баннер</a> отображаемый на главной странице магазина. Рекомендуемое соотношение сторон для изображений - 2.5:1 (например 500×200).
HTML, HTML,
], ],
], ],

View File

@@ -42,7 +42,7 @@ settings.load()
} }
}) })
.then(() => { .then(() => {
console.log('Set theme attributes'); console.debug('[Init] Set theme attributes');
document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]); document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]);
if (settings.night_auto) { if (settings.night_auto) {
window.Telegram.WebApp.onEvent('themeChanged', function () { window.Telegram.WebApp.onEvent('themeChanged', function () {
@@ -55,7 +55,7 @@ settings.load()
} }
}) })
.then(() => { .then(() => {
console.log('Load front page categories and products.'); console.debug('[Init] Load front page categories and products.');
const categoriesStore = useCategoriesStore(); const categoriesStore = useCategoriesStore();
categoriesStore.fetchTopCategories(); categoriesStore.fetchTopCategories();
}) })

View File

@@ -8,6 +8,7 @@ import Checkout from "@/views/Checkout.vue";
import OrderCreated from "@/views/OrderCreated.vue"; import OrderCreated from "@/views/OrderCreated.vue";
import Search from "@/views/Search.vue"; import Search from "@/views/Search.vue";
import Filters from "@/views/Filters.vue"; import Filters from "@/views/Filters.vue";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
const routes = [ const routes = [
{ {

View File

@@ -0,0 +1,56 @@
import {defineStore} from "pinia";
import {useSettingsStore} from "@/stores/SettingsStore.js";
export const useYaMetrikaStore = defineStore('ya_metrika', {
state: () => ({
queue: [],
}),
actions: {
pushHit(url, params = {}) {
const settings = useSettingsStore();
if (!settings.ya_metrika_enabled) {
console.debug('[ym] Yandex Metrika disabled in settings.');
return;
}
const fullUrl = `/#${url}`;
if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) {
console.debug('[ym] Hit ', fullUrl);
console.debug('[ym] ID ', window.YA_METRIKA_ID);
console.debug('[ym] params ', params);
window.ym(window.YA_METRIKA_ID, 'hit', fullUrl, params);
} else {
console.debug('[ym] Yandex Metrika is not initialized. Pushed to queue.');
this.queue.push({
event: 'hit',
payload: {
fullUrl,
params,
}
});
}
},
processQueue() {
if (this.queue.length === 0) {
return;
}
console.debug('[ym] Start processing queue. Size: ', this.queue.length);
while (this.queue.length > 0) {
const item = this.queue.shift();
if (item.event === 'hit') {
console.debug('[ym] Queue ', item);
window.ym(window.YA_METRIKA_ID, item.event, item.payload.url, item.payload.params);
} else {
console.error('[ym] Unsupported queue event: ', item.event);
}
}
console.debug('[ym] Queue processing complete. Size: ', this.queue.length);
},
},
});

View File

@@ -1,7 +1,28 @@
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
function getMetrikaId() {
// Пробуем найти все элементы <script> с mc.yandex.ru
const scripts = Array.from(document.scripts);
for (const s of scripts) {
if (s.src.includes('mc.yandex.ru/metrika/tag.js')) {
const match = s.src.match(/id=(\d+)/);
if (match) return match[1];
}
}
return null;
}
export function injectYaMetrika() { export function injectYaMetrika() {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = '/index.php?route=extension/tgshop/handle/ya_metrika'; script.src = '/index.php?route=extension/tgshop/handle/ya_metrika';
script.async = true; // script.async = true;
document.head.appendChild(script); document.head.appendChild(script);
console.log('Yandex Metrika injected to the page.'); console.debug('[Init] Yandex Metrika injected to the page.');
script.onload = () => {
window.YA_METRIKA_ID = getMetrikaId();
console.debug('[Init] Detected Yandex.Metrika ID:', window.YA_METRIKA_ID);
const yaMetrika = useYaMetrikaStore();
yaMetrika.processQueue();
}
} }

View File

@@ -138,7 +138,7 @@ import Quantity from "@/components/Quantity.vue";
import OptionRadio from "@/components/ProductOptions/Cart/OptionRadio.vue"; import OptionRadio from "@/components/ProductOptions/Cart/OptionRadio.vue";
import OptionCheckbox from "@/components/ProductOptions/Cart/OptionCheckbox.vue"; import OptionCheckbox from "@/components/ProductOptions/Cart/OptionCheckbox.vue";
import OptionText from "@/components/ProductOptions/Cart/OptionText.vue"; import OptionText from "@/components/ProductOptions/Cart/OptionText.vue";
import {computed} from "vue"; import {computed, onMounted} from "vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useSettingsStore} from "@/stores/SettingsStore.js"; import {useSettingsStore} from "@/stores/SettingsStore.js";
@@ -166,6 +166,10 @@ function removeItem(cartId) {
function goToCheckout() { function goToCheckout() {
router.push({name: 'checkout'}); router.push({name: 'checkout'});
} }
onMounted(async () => {
window.document.title = 'Корзина покупок';
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -105,6 +105,7 @@ function showProductsInParentCategory() {
} }
onMounted(async () => { onMounted(async () => {
window.document.title = 'Каталог';
await categoriesStore.fetchCategories(); await categoriesStore.fetchCategories();
}); });
</script> </script>

View File

@@ -70,7 +70,7 @@ import {useCheckoutStore} from "@/stores/CheckoutStore.js";
import TgInput from "@/components/Form/TgInput.vue"; import TgInput from "@/components/Form/TgInput.vue";
import TgTextarea from "@/components/Form/TgTextarea.vue"; import TgTextarea from "@/components/Form/TgTextarea.vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {computed, ref} from "vue"; import {computed, onMounted, ref} from "vue";
import {IMaskComponent} from "vue-imask"; import {IMaskComponent} from "vue-imask";
const checkout = useCheckoutStore(); const checkout = useCheckoutStore();
@@ -90,4 +90,8 @@ async function onCreateBtnClick() {
error.value = 'Невозможно создать заказ.'; error.value = 'Невозможно создать заказ.';
} }
} }
onMounted(async () => {
window.document.title = 'Оформление заказа';
});
</script> </script>

View File

@@ -87,6 +87,7 @@ const resetFilters = async () => {
onMounted(async () => { onMounted(async () => {
console.debug('Filters: OnMounted'); console.debug('Filters: OnMounted');
window.document.title = 'Фильтры';
if (filtersStore.applied?.rules) { if (filtersStore.applied?.rules) {
console.debug('Filters: Found applied filters.'); console.debug('Filters: Found applied filters.');

View File

@@ -31,12 +31,13 @@
<script setup> <script setup>
import ProductsList from "@/components/ProductsList.vue"; import ProductsList from "@/components/ProductsList.vue";
import CategoriesInline from "../components/CategoriesInline.vue"; import CategoriesInline from "../components/CategoriesInline.vue";
import {onMounted, ref, toRaw} from "vue"; import {nextTick, onActivated, onMounted, ref, toRaw} from "vue";
import IconFunnel from "@/components/Icons/IconFunnel.vue"; import IconFunnel from "@/components/Icons/IconFunnel.vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import ftch from "@/utils/ftch.js"; import ftch from "@/utils/ftch.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import Banner from "@/components/Banner.vue"; import Banner from "@/components/Banner.vue";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
defineOptions({ defineOptions({
name: 'Home' name: 'Home'
@@ -44,6 +45,7 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const filtersStore = useProductFiltersStore(); const filtersStore = useProductFiltersStore();
const yaMetrika = useYaMetrikaStore();
const haptic = window.Telegram.WebApp.HapticFeedback; const haptic = window.Telegram.WebApp.HapticFeedback;
const products = ref([]); const products = ref([]);
@@ -96,9 +98,14 @@ async function onLoadMore() {
} }
} }
onActivated(() => {
yaMetrika.pushHit('/');
});
onMounted(async () => { onMounted(async () => {
console.debug("Home: Home Mounted"); window.document.title = 'Главная страница';
console.debug("Home: Scroll top"); console.debug("[Home] Home Mounted");
console.debug("[Home] Scroll top");
await fetchProducts(); await fetchProducts();
window.scrollTo(0, 0); window.scrollTo(0, 0);
}); });

View File

@@ -38,6 +38,11 @@
<script setup> <script setup>
import {useCheckoutStore} from "@/stores/CheckoutStore.js"; import {useCheckoutStore} from "@/stores/CheckoutStore.js";
import {onMounted} from "vue";
const checkout = useCheckoutStore(); const checkout = useCheckoutStore();
onMounted(async () => {
window.document.title = 'Заказ оформлен';
});
</script> </script>

View File

@@ -130,8 +130,10 @@
> >
<template v-if="product.share"> <template v-if="product.share">
Открыть товар Открыть товар
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /> stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>
</svg> </svg>
</template> </template>
@@ -154,7 +156,7 @@
</template> </template>
<script setup> <script setup>
import {computed, onMounted, onUnmounted, ref} from "vue"; import {computed, nextTick, onMounted, onUnmounted, ref} from "vue";
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import ProductOptions from "../components/ProductOptions/ProductOptions.vue"; import ProductOptions from "../components/ProductOptions/ProductOptions.vue";
import {useCartStore} from "../stores/CartStore.js"; import {useCartStore} from "../stores/CartStore.js";
@@ -165,6 +167,7 @@ import FullScreenImageViewer from "@/components/FullScreenImageViewer.vue";
import LoadingFullScreen from "@/components/LoadingFullScreen.vue"; import LoadingFullScreen from "@/components/LoadingFullScreen.vue";
import ProductNotFound from "@/components/ProductNotFound.vue"; import ProductNotFound from "@/components/ProductNotFound.vue";
import {useSettingsStore} from "@/stores/SettingsStore.js"; import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
const route = useRoute(); const route = useRoute();
const productId = computed(() => route.params.id); const productId = computed(() => route.params.id);
@@ -179,6 +182,7 @@ const isFullScreen = ref(false);
const initialFullScreenIndex = ref(0); const initialFullScreenIndex = ref(0);
const isLoading = ref(false); const isLoading = ref(false);
const settings = useSettingsStore(); const settings = useSettingsStore();
const yaMetrika = useYaMetrikaStore();
const canAddToCart = computed(() => { const canAddToCart = computed(() => {
if (!product.value || product.value.options === undefined || product.value.options?.length === 0) { if (!product.value || product.value.options === undefined || product.value.options?.length === 0) {
@@ -227,11 +231,11 @@ async function actionBtnClick() {
} }
function openProductInMarketplace() { function openProductInMarketplace() {
if (! product.value.share) { if (!product.value.share) {
return; return;
} }
window.Telegram.WebApp.openLink(product.value.share, { try_instant_view: false }); window.Telegram.WebApp.openLink(product.value.share, {try_instant_view: false});
} }
function setQuantity(newQuantity) { function setQuantity(newQuantity) {
@@ -261,6 +265,16 @@ onMounted(async () => {
try { try {
const {data} = await apiFetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`); const {data} = await apiFetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`);
product.value = data; product.value = data;
window.document.title = data.name;
yaMetrika.pushHit(route.path, {
title: data.name,
params: {
'Название товара': data.name,
'ИД товара': data.product_id,
'Цена': data.price,
},
});
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {

View File

@@ -73,9 +73,13 @@
<script setup> <script setup>
import {useSearchStore} from "@/stores/SearchStore.js"; import {useSearchStore} from "@/stores/SearchStore.js";
import {useDebounceFn} from "@vueuse/core"; import {useDebounceFn} from "@vueuse/core";
import {ref} from "vue"; import {onMounted, ref} from "vue";
const searchStore = useSearchStore(); const searchStore = useSearchStore();
const searchInput = ref(null); const searchInput = ref(null);
const debouncedSearch = useDebounceFn(() => searchStore.performSearch(), 500); const debouncedSearch = useDebounceFn(() => searchStore.performSearch(), 500);
onMounted(async () => {
window.document.title = 'Поиск';
});
</script> </script>