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

View File

@@ -85,9 +85,9 @@
{# 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) }}"
data-placeholder="{{ attribute(_context, settingKey) }}"
data-placeholder="https://placehold.co/100x100?text=Удалено"
/>
</a>
<input type="hidden"
@@ -95,8 +95,7 @@
value="{{ attribute(_context, settingKey) }}"
id="{{ settingKey }}"
/>
{# Image #}
{# Textarea #}
{% elseif item['type'] == 'textarea' %}
<textarea name="{{ settingKey }}"
rows="{{ item['rows'] }}"
@@ -104,7 +103,6 @@
id="{{ settingKey }}"
class="form-control"
>{{ attribute(_context, settingKey) }}</textarea>
{# Products #}
{% elseif item['type'] == 'products' %}
<input type="text" value="" placeholder="Начните вводить название товара..." id="{{ settingKey }}-input" class="form-control"/>
@@ -451,3 +449,12 @@
</div>
</div>
{{ 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['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;
$appIcon = $this->imageTool->resize($appIcon, 32, 32, 'no_image.png', 'png'). '?_v=' . $hash;
}
return new JsonResponse([
'app_name' => $this->settings->get('app_name'),
'app_icon' => $appIcon ? $appIcon . '?_v=' . $hash : '',
'app_icon' => $appIcon ?? '',
'app_icon192' => $icons['icon192'] ?? '',
'app_icon180' => $icons['icon180'] ?? '',
'app_icon152' => $icons['icon152'] ?? '',

View File

@@ -1,19 +1,36 @@
<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">
<header class="app-header w-full" v-if="platform === 'ios'"></header>
<section class="safe-top">
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
<Navbar @drawer="toggleDrawer"/>
<section class="telecart-main-section">
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'" />
<RouterView v-slot="{ Component, route }">
<KeepAlive include="Home" :key="filtersStore.paramsHashForRouter">
<component :is="Component" :key="route.fullPath"/>
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
<CartButton v-if="settings.store_enabled"/>
<Dock v-if="isAppDockShown"/>
<CartButton v-if="settings.store_enabled" />
<Dock v-if="isAppDockShown" />
</section>
</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>
<script setup>
@@ -25,6 +42,7 @@ import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import CartButton from "@/components/CartButton.vue";
import Dock from "@/components/Dock.vue";
import Navbar from "@/components/Navbar.vue";
const tg = useMiniApp();
const platform = ref();
@@ -39,6 +57,7 @@ const settings = useSettingsStore();
const filtersStore = useProductFiltersStore();
const backButton = window.Telegram.WebApp.BackButton;
const haptic = window.Telegram.WebApp.HapticFeedback;
const drawerOpen = ref(false);
const routesToHideAppDock = [
'product.show',
@@ -56,6 +75,10 @@ function navigateBack() {
router.back();
}
function toggleDrawer() {
drawerOpen.value = !drawerOpen.value
}
watch(
() => route.name,
() => {
@@ -71,7 +94,7 @@ watch(
);
function handleClickOutside(e) {
if (!e.target.closest('input')) {
if (!e.target.closest('input,select,textarea')) {
document.activeElement?.blur();
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="telecart-dock fixed bottom-0 w-full z-50 px-10">
<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
:to="{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-fraction-color: var(--color-neutral-content);
--product_list_title_max_lines: 1;
--tc-navbar-min-height: 3rem;
}
.swiper-pagination-bullets {
@@ -41,14 +42,22 @@ html {
}
.app-header {
z-index: 100;
z-index: 60;
position: fixed;
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;
justify-content: center;
flex-direction: column;
justify-content: end;
align-items: center;
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 {

View File

@@ -109,7 +109,7 @@
</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
class="btn btn-primary select-none shadow-xl"
:disabled="cart.canCheckout === false"
@@ -167,3 +167,9 @@ function goToCheckout() {
router.push({name: 'checkout'});
}
</script>
<style scoped>
.btn-checkout {
bottom: calc(var(--spacing) * 22 + var(--tg-safe-area-inset-bottom))
}
</style>

View File

@@ -1,11 +1,10 @@
<template>
<div class="max-w-3xl mx-auto space-y-6 pb-30">
<h2 class="text-2xl text-center">
<div class="max-w-3xl mx-auto p-4 space-y-6 pb-20">
<h2 class="text-2xl mb-5 text-center">
Оформление заказа
</h2>
<div class="card card-border bg-base-100 w-full">
<div class="card-body">
<div class="w-full">
<TgInput
v-model="checkout.customer.firstName"
placeholder="Введите имя"
@@ -50,7 +49,6 @@
@clearError="checkout.clearError('comment')"
/>
</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">