feat(spa): show navbar with app logo and app name

This commit is contained in:
2025-10-25 18:48:47 +03:00
parent ed8592c19d
commit c3c0d6d2c1
9 changed files with 174 additions and 76 deletions

View File

@@ -378,24 +378,22 @@ TEXT,
], ],
'module_tgshop_app_name' => [ 'module_tgshop_app_name' => [
'hidden' => true,
'required' => true,
'type' => 'text', 'type' => 'text',
'placeholder' => 'Введите название Телеграм магазина', 'placeholder' => 'Без названия',
'help' => <<<TEXT 'help' => <<<TEXT
Отображается в заголовке Telegram Mini App при запуске, а также используется как подпись Отображается в заголовке Telegram Mini App при запуске, а также используется как подпись
под иконкой, если пользователь добавит приложение на главный экран своего устройства. под иконкой, если пользователь добавит приложение на главный экран своего устройства.
Рекомендуется короткое и понятное название (до 20 символов). Рекомендуется короткое и понятное название (до 20 символов).
Если оставить пустым, то название выводиться не будет.
TEXT, TEXT,
], ],
'module_tgshop_app_icon' => [ 'module_tgshop_app_icon' => [
'hidden' => true,
'type' => 'image', 'type' => 'image',
'help' => <<<TEXT 'help' => <<<TEXT
Изображение, которое будет отображаться в Telegram Mini App и на рабочем столе устройства, Изображение, которое будет отображаться в Telegram Mini App и на рабочем столе устройства,
если пользователь добавит приложение как ярлык. Используйте квадратное изображение PNG или SVG, если пользователь добавит приложение как ярлык. Рекомендуется использовать квадратное изображение PNG или SVG,
размером не менее 192×192 пикселей, а лучше 512x512. размером 32×32 пикселей.
TEXT, TEXT,
], ],

View File

@@ -85,9 +85,9 @@
{# Image #} {# Image #}
{% elseif item['type'] == 'image' %} {% elseif item['type'] == 'image' %}
<a href="" id="thumb-image" data-toggle="image" class="img-thumbnail"> <a href="" id="thumb-image-{{ settingKey }}" data-toggle="image" class="img-thumbnail">
<img src="{{ attribute(_context, settingKey) }}" <img src="{{ attribute(_context, settingKey) }}"
data-placeholder="{{ attribute(_context, settingKey) }}" data-placeholder="https://placehold.co/100x100?text=Удалено"
/> />
</a> </a>
<input type="hidden" <input type="hidden"
@@ -95,8 +95,7 @@
value="{{ attribute(_context, settingKey) }}" value="{{ attribute(_context, settingKey) }}"
id="{{ settingKey }}" id="{{ settingKey }}"
/> />
{# Textarea #}
{# Image #}
{% elseif item['type'] == 'textarea' %} {% elseif item['type'] == 'textarea' %}
<textarea name="{{ settingKey }}" <textarea name="{{ settingKey }}"
rows="{{ item['rows'] }}" rows="{{ item['rows'] }}"
@@ -104,7 +103,6 @@
id="{{ settingKey }}" id="{{ settingKey }}"
class="form-control" class="form-control"
>{{ attribute(_context, settingKey) }}</textarea> >{{ attribute(_context, settingKey) }}</textarea>
{# Products #} {# Products #}
{% elseif item['type'] == 'products' %} {% elseif item['type'] == 'products' %}
<input type="text" value="" placeholder="Начните вводить название товара..." id="{{ settingKey }}-input" class="form-control"/> <input type="text" value="" placeholder="Начните вводить название товара..." id="{{ settingKey }}-input" class="form-control"/>
@@ -451,3 +449,12 @@
</div> </div>
</div> </div>
{{ footer }} {{ footer }}
<script>
const $element = $('#thumb-image-module_tgshop_app_icon');
$('#button-clear').on('click', function() {
$element.find('img').attr('src', $element.find('img').attr('data-placeholder'));
$element.parent().find('input').val('');
$element.popover('destroy');
});
</script>

View File

@@ -37,11 +37,12 @@ class SettingsHandler
$icons['icon180'] = $this->imageTool->resize($appIcon, 180, 180, 'no_image.png', 'png'). '?_v=' . $hash; $icons['icon180'] = $this->imageTool->resize($appIcon, 180, 180, 'no_image.png', 'png'). '?_v=' . $hash;
$icons['icon152'] = $this->imageTool->resize($appIcon, 152, 152, 'no_image.png', 'png'). '?_v=' . $hash; $icons['icon152'] = $this->imageTool->resize($appIcon, 152, 152, 'no_image.png', 'png'). '?_v=' . $hash;
$icons['icon120'] = $this->imageTool->resize($appIcon, 120, 120, 'no_image.png', 'png'). '?_v=' . $hash; $icons['icon120'] = $this->imageTool->resize($appIcon, 120, 120, 'no_image.png', 'png'). '?_v=' . $hash;
$appIcon = $this->imageTool->resize($appIcon, 32, 32, 'no_image.png', 'png'). '?_v=' . $hash;
} }
return new JsonResponse([ return new JsonResponse([
'app_name' => $this->settings->get('app_name'), 'app_name' => $this->settings->get('app_name'),
'app_icon' => $appIcon ? $appIcon . '?_v=' . $hash : '', 'app_icon' => $appIcon ?? '',
'app_icon192' => $icons['icon192'] ?? '', 'app_icon192' => $icons['icon192'] ?? '',
'app_icon180' => $icons['icon180'] ?? '', 'app_icon180' => $icons['icon180'] ?? '',
'app_icon152' => $icons['icon152'] ?? '', 'app_icon152' => $icons['icon152'] ?? '',

View File

@@ -1,19 +1,36 @@
<template> <template>
<div class="drawer h-full">
<input id="app-drawer" type="checkbox" class="drawer-toggle" v-model="drawerOpen" />
<div class="drawer-content">
<div class="app-container h-full"> <div class="app-container h-full">
<header class="app-header w-full" v-if="platform === 'ios'"></header> <header class="app-header w-full" v-if="platform === 'ios'"></header>
<section class="safe-top"> <Navbar @drawer="toggleDrawer"/>
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
<section class="telecart-main-section">
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'" />
<RouterView v-slot="{ Component, route }"> <RouterView v-slot="{ Component, route }">
<KeepAlive include="Home" :key="filtersStore.paramsHashForRouter"> <KeepAlive include="Home" :key="filtersStore.paramsHashForRouter">
<component :is="Component" :key="route.fullPath"/> <component :is="Component" :key="route.fullPath" />
</KeepAlive> </KeepAlive>
</RouterView> </RouterView>
<CartButton v-if="settings.store_enabled"/> <CartButton v-if="settings.store_enabled" />
<Dock v-if="isAppDockShown"/> <Dock v-if="isAppDockShown" />
</section> </section>
</div> </div>
</div>
<div class="drawer-side z-50 safe-top">
<label for="app-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
<li><a href="#">🏠 Главная</a></li>
<li><a href="#">🛒 Корзина</a></li>
<li><a @click="drawerOpen = false"> Закрыть</a></li>
</ul>
</div>
</div>
</template> </template>
<script setup> <script setup>
@@ -25,6 +42,7 @@ import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import CartButton from "@/components/CartButton.vue"; import CartButton from "@/components/CartButton.vue";
import Dock from "@/components/Dock.vue"; import Dock from "@/components/Dock.vue";
import Navbar from "@/components/Navbar.vue";
const tg = useMiniApp(); const tg = useMiniApp();
const platform = ref(); const platform = ref();
@@ -39,6 +57,7 @@ const settings = useSettingsStore();
const filtersStore = useProductFiltersStore(); const filtersStore = useProductFiltersStore();
const backButton = window.Telegram.WebApp.BackButton; const backButton = window.Telegram.WebApp.BackButton;
const haptic = window.Telegram.WebApp.HapticFeedback; const haptic = window.Telegram.WebApp.HapticFeedback;
const drawerOpen = ref(false);
const routesToHideAppDock = [ const routesToHideAppDock = [
'product.show', 'product.show',
@@ -56,6 +75,10 @@ function navigateBack() {
router.back(); router.back();
} }
function toggleDrawer() {
drawerOpen.value = !drawerOpen.value
}
watch( watch(
() => route.name, () => route.name,
() => { () => {
@@ -71,7 +94,7 @@ watch(
); );
function handleClickOutside(e) { function handleClickOutside(e) {
if (!e.target.closest('input')) { if (!e.target.closest('input,select,textarea')) {
document.activeElement?.blur(); document.activeElement?.blur();
} }
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="telecart-dock fixed bottom-0 w-full z-50 px-10"> <div class="telecart-dock fixed bottom-0 w-full z-50 px-10">
<div <div
class="telecart-dock-inner flex justify-between items-center bg-base-300/10 h-full backdrop-blur-md px-2 border-base-300/90 border"> class="telecart-dock-inner flex justify-around items-center bg-base-300/10 h-full backdrop-blur-md border-base-300/90 border">
<RouterLink <RouterLink
:to="{name: 'home'}" :to="{name: 'home'}"
:class="{'active': route.name === 'home'}" :class="{'active': route.name === 'home'}"

View File

@@ -0,0 +1,56 @@
<template>
<div class="telecart-navbar fixed navbar bg-primary text-primary-content z-50 shadow-md" :class="{'pb-0' : platform !== 'ios'}">
<div class="navbar-start">
<div v-if="false" class="dropdown">
<button class="btn btn-ghost btn-circle" @click="toggleDrawer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" /> </svg>
</button>
</div>
</div>
<div class="navbar-center">
<RouterLink :to="{name: 'home'}" class="text-xl flex items-center">
<div class="avatar mr-2">
<div v-if="settings.app_icon" class="h-8 rounded-full bg-base-100">
<img :src="settings.app_icon" class="h-8" alt=""/>
</div>
</div>
{{ settings.app_name }}
</RouterLink>
</div>
<div class="navbar-end">
<button v-if="false" class="btn btn-ghost btn-circle">
<div class="indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> </svg>
<span class="badge badge-xs badge-secondary indicator-item">1</span>
</div>
</button>
</div>
</div>
</template>
<script setup>
import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useMiniApp} from "vue-tg";
import {ref} from "vue";
const settings = useSettingsStore();
const emits = defineEmits(['drawer']);
const tg = useMiniApp();
const platform = ref();
platform.value = tg.platform;
function toggleDrawer() {
emits('drawer');
}
</script>
<style scoped>
.telecart-navbar {
padding-top: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
min-height: var(--tc-navbar-min-height);
}
</style>

View File

@@ -14,6 +14,7 @@ html {
--swiper-pagination-bullet-inactive-color: var(--color-base-content); --swiper-pagination-bullet-inactive-color: var(--color-base-content);
--swiper-pagination-fraction-color: var(--color-neutral-content); --swiper-pagination-fraction-color: var(--color-neutral-content);
--product_list_title_max_lines: 1; --product_list_title_max_lines: 1;
--tc-navbar-min-height: 3rem;
} }
.swiper-pagination-bullets { .swiper-pagination-bullets {
@@ -41,14 +42,22 @@ html {
} }
.app-header { .app-header {
z-index: 100; z-index: 60;
position: fixed; position: fixed;
background: var(--color-primary); background: var(--color-primary);
padding-top: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top)); height: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
min-height: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
max-height: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
display: flex; display: flex;
justify-content: center; flex-direction: column;
justify-content: end;
align-items: center; align-items: center;
color: white; color: white;
padding-bottom: 8px;
}
.telecart-main-section {
padding-top: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top) + var(--tc-navbar-min-height));
} }
.swiper-pagination-bullets > .swiper-pagination-bullet { .swiper-pagination-bullets > .swiper-pagination-bullet {

View File

@@ -109,7 +109,7 @@
</div> </div>
</div> </div>
<div class="fixed px-4 pt-4 bottom-25 left-0 w-full z-50 flex justify-end items-center gap-2"> <div class="btn-checkout fixed px-4 pt-4 left-0 w-full z-50 flex justify-end items-center gap-2">
<button <button
class="btn btn-primary select-none shadow-xl" class="btn btn-primary select-none shadow-xl"
:disabled="cart.canCheckout === false" :disabled="cart.canCheckout === false"
@@ -167,3 +167,9 @@ function goToCheckout() {
router.push({name: 'checkout'}); router.push({name: 'checkout'});
} }
</script> </script>
<style scoped>
.btn-checkout {
bottom: calc(var(--spacing) * 22 + var(--tg-safe-area-inset-bottom))
}
</style>

View File

@@ -1,11 +1,10 @@
<template> <template>
<div class="max-w-3xl mx-auto space-y-6 pb-30"> <div class="max-w-3xl mx-auto p-4 space-y-6 pb-20">
<h2 class="text-2xl text-center"> <h2 class="text-2xl mb-5 text-center">
Оформление заказа Оформление заказа
</h2> </h2>
<div class="card card-border bg-base-100 w-full"> <div class="w-full">
<div class="card-body">
<TgInput <TgInput
v-model="checkout.customer.firstName" v-model="checkout.customer.firstName"
placeholder="Введите имя" placeholder="Введите имя"
@@ -50,7 +49,6 @@
@clearError="checkout.clearError('comment')" @clearError="checkout.clearError('comment')"
/> />
</div> </div>
</div>
<div <div
class="fixed px-4 pb-10 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex flex-col justify-between items-center gap-2 border-t-1 border-t-base-300"> class="fixed px-4 pb-10 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex flex-col justify-between items-center gap-2 border-t-1 border-t-base-300">