feat(orders): tg notifications, ya metrika, meta tags

This commit is contained in:
Nikita Kiselev
2025-08-03 09:39:51 +03:00
parent 454bd39f1f
commit 86d0fa9594
32 changed files with 1205 additions and 76 deletions

View File

@@ -22,22 +22,21 @@ disableVerticalSwipes();
const router = useRouter();
const route = useRoute();
const backButton = useBackButton();
watch(
() => route,
() => route.name,
() => {
if (route.name === 'home') {
backButton.hide?.();
window.Telegram.WebApp.BackButton.hide();
window.Telegram.WebApp.BackButton.offClick();
} else {
backButton.show?.();
backButton.onClick?.(() => {
window.Telegram.WebApp.BackButton.show();
window.Telegram.WebApp.BackButton.onClick(() => {
window.Telegram.WebApp.HapticFeedback.impactOccurred('light');
router.back();
});
}
},
{immediate: true, deep: true}
{immediate: true}
);
</script>

View File

@@ -0,0 +1,21 @@
<template>
<div style="z-index: 99999" class="fixed top-0 left-0 w-full h-full bg-base-100">
<div class="flex flex-col items-center justify-center h-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-20">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<h1 class="font-semibold text-2xl mb-2">Магазин временно недоступен</h1>
<p class="text-sm text-muted">Мы на перерыве, скоро всё снова заработает 🛠</p>
</div>
</div>
</template>
<script setup>
const props = defineProps({
error: Error,
});
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="route.name !== 'cart.show'" class="fixed right-2 bottom-30 z-50 opacity-90">
<div v-if="isCartBtnShow" class="fixed right-2 bottom-30 z-50 opacity-90">
<div class="indicator">
<span class="indicator-item indicator-top indicator-start badge badge-secondary">{{ cart.productsCount }}</span>
<button class="btn btn-primary btn-lg btn-circle" @click="openCart">
@@ -15,7 +15,7 @@
</template>
<script setup>
import {onMounted} from "vue";
import {computed, onMounted} from "vue";
import {useCartStore} from "@/stores/CartStore.js";
import {useRoute, useRouter} from "vue-router";
@@ -23,6 +23,11 @@ const cart = useCartStore();
const router = useRouter();
const route = useRoute();
const isCartBtnShow = computed(() => {
return route.name !== 'cart.show' && route.name !== 'checkout';
});
function openCart() {
window.Telegram.WebApp.HapticFeedback.selectionChanged();
router.push({name: 'cart.show'});

View File

@@ -11,7 +11,3 @@
<script setup>
</script>
<style scoped>
</style>

View File

@@ -104,11 +104,8 @@ watch(() => route.params.id, async newId => {
onMounted(async () => {
const saved = productsStore.savedCategoryId === categoryId;
console.log("Saved Category: ", saved);
if (saved && productsStore.products.data.length > 0) {
await nextTick();
console.log("Products exists, scrolling to ", productsStore.savedScrollY);
// повторяем до тех пор, пока высота не станет больше savedScrollY
const interval = setInterval(() => {
const maxScroll = document.documentElement.scrollHeight - window.innerHeight

View File

@@ -1,9 +1,15 @@
import { createApp } from 'vue'
import {createApp} from 'vue'
import App from './App.vue'
import './style.css'
import { VueTelegramPlugin } from 'vue-tg';
import { router } from './router';
import { createPinia } from 'pinia';
import {VueTelegramPlugin} from 'vue-tg';
import {router} from './router';
import {createPinia} from 'pinia';
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";
import ApplicationError from "@/ApplicationError.vue";
import AppMetaInitializer from "@/utils/AppMetaInitializer.ts";
import {injectYaMetrika} from "@/utils/yaMetrika.js";
const pinia = createPinia();
const app = createApp(App);
@@ -12,22 +18,26 @@ app
.use(router)
.use(VueTelegramPlugin);
app.mount('#app');
const settings = useSettingsStore();
const categoriesStore = useCategoriesStore();
categoriesStore.fetchTopCategories();
categoriesStore.fetchCategories();
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";
if (settings.night_auto) {
window.Telegram.WebApp.onEvent('themeChanged', function () {
document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]);
settings.load()
.then(() => {
document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]);
if (settings.night_auto) {
window.Telegram.WebApp.onEvent('themeChanged', function () {
document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]);
});
}
})
.then(() => new AppMetaInitializer(settings).init())
.then(() => app.mount('#app'))
.then(() => window.Telegram.WebApp.ready())
.then(() => settings.ya_metrika_enabled && injectYaMetrika())
.catch(error => {
console.error(error);
const errorApp = createApp(ApplicationError, {error});
errorApp.mount('#app-error');
});
} else {
document.documentElement.setAttribute('data-theme', settings.theme.light);
}
window.Telegram.WebApp.ready();

View File

@@ -12,6 +12,7 @@ export const useCheckoutStore = defineStore('checkout', {
phone: "+79999999999",
address: "Москва, Красная площадь, 1",
comment: "Доставить срочно❗️",
tgData: null,
},
validationErrors: {},
@@ -26,6 +27,19 @@ export const useCheckoutStore = defineStore('checkout', {
actions: {
async makeOrder() {
try {
const data = window.Telegram.WebApp.initDataUnsafe;
if (! data.allows_write_to_pm) {
await window.Telegram.WebApp.requestWriteAccess((granted) => {
if (granted) {
console.log('Пользователь разрешил отправку сообщений');
} else {
alert('Вы не дали разрешение — бот не сможет отправлять вам уведомления');
}
});
}
this.customer.tgData = data.user;
await storeOrder(this.customer);
await window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
await useCartStore().getProducts();
@@ -44,6 +58,6 @@ export const useCheckoutStore = defineStore('checkout', {
clearError(field) {
this.validationErrors[field] = null;
}
},
},
});

View File

@@ -1,12 +1,37 @@
import {defineStore} from "pinia";
import {fetchSettings} from "@/utils/ftch.js";
export const useSettingsStore = defineStore('settings', {
state: () => ({
app_name: 'OpenCart Telegram магазин',
app_icon: '',
app_icon192: '',
app_icon180: '',
app_icon152: '',
app_icon120: '',
manifest_url: null,
night_auto: true,
ya_metrika_enabled: false,
theme: {
light: 'light',
dark: 'dark',
},
noMoreProductsMessage: '🔚 Ну всё, разгрузили всё, что было. Даже кладовщика разбудить не удалось.',
}),
actions: {
async load() {
const settings = await fetchSettings();
this.manifest_url = settings.manifest_url;
this.app_name = settings.app_name;
this.app_icon = settings.app_icon;
this.app_icon192 = settings.app_icon192;
this.app_icon180 = settings.app_icon180;
this.app_icon152 = settings.app_icon152;
this.app_icon120 = settings.app_icon120;
this.theme.light = settings.theme_light;
this.theme.dark = settings.theme_dark;
this.ya_metrika_enabled = settings.ya_metrika_enabled;
}
}
});

View File

@@ -0,0 +1,46 @@
class AppMetaInitializer {
private readonly settings: object;
constructor(settings: object) {
this.settings = settings;
}
public init() {
document.title = this.settings.app_name;
this.setMeta('application-name', this.settings.app_name);
this.setMeta('apple-mobile-web-app-title', this.settings.app_name);
this.setMeta('mobile-web-app-capable', 'yes');
this.setMeta('apple-mobile-web-app-capable', 'yes');
this.setMeta('apple-mobile-web-app-status-bar-style', 'default');
this.setMeta('theme-color', '#000000');
this.setMeta('msapplication-navbutton-color', '#000000');
this.setMeta('apple-mobile-web-app-status-bar-style', 'black-translucent');
this.addLink('manifest', this.settings.manifest_url);
this.addLink('icon', this.settings.app_icon192, '192x192');
this.addLink('apple-touch-icon', this.settings.app_icon192);
this.addLink('apple-touch-icon', this.settings.app_icon180, '180x180');
this.addLink('apple-touch-icon', this.settings.app_icon152, '152x152');
this.addLink('apple-touch-icon', this.settings.app_icon120, '120x120');
}
private setMeta(name: string, content: string) {
let meta = document.querySelector(`meta[name="${name}"]`);
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', name);
document.head.appendChild(meta);
}
meta.setAttribute('content', content);
}
private addLink(rel: string, href: string, sizes?: string) {
const link = document.createElement('link');
link.rel = rel;
link.href = href;
if (sizes) link.sizes = sizes;
document.head.appendChild(link);
}
}
export default AppMetaInitializer;

View File

@@ -59,4 +59,8 @@ export async function cartEditItem(data) {
});
}
export async function fetchSettings() {
return await ftch('settings');
}
export default ftch;

View File

@@ -0,0 +1,7 @@
export function injectYaMetrika() {
const script = document.createElement('script');
script.src = '/index.php?route=extension/tgshop/handle/ya_metrika';
script.async = true;
document.head.appendChild(script);
console.log('Yandex Metrika injected to the page.');
}