mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 19:24:47 +00:00
universal entry: bot menu button + role chooser в MiniApp
Перешли на единый универсальный паттерн вместо reply/inline-keyboard: 1. Bot menu-button — постоянная кнопка «ЗОВ» слева от input в чате (set_chat_menu_button с WebAppInfo). Видна на ВСЕХ платформах: Telegram Desktop, iOS, Android, Web. Один тап — открывает MiniApp. 2. MiniApp без ?role= в URL показывает role chooser как первый экран: три большие карточки [Я менеджер] [Я клиент] [Я сотрудник]. Тап → URL получает ?role=X → re-run init() → загрузка кабинета с правильно подписанным initData. Решение универсальное — не зависит от reply/inline-кнопок и их поведения с initData на разных клиентах Telegram. Cache bust v=20260513n.
This commit is contained in:
parent
d6fbc3df13
commit
b352c4927f
14
bot/main.py
14
bot/main.py
@ -4,6 +4,7 @@ import logging
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.types import MenuButtonWebApp, WebAppInfo
|
||||
|
||||
from config import load_config
|
||||
from handlers import start
|
||||
@ -28,6 +29,19 @@ async def main() -> None:
|
||||
if config.use_webhook:
|
||||
raise NotImplementedError("Webhook mode будет добавлен после MVP")
|
||||
|
||||
# Универсальная меню-кнопка — открывает MiniApp одним тапом.
|
||||
# Внутри MiniApp пользователь выбирает роль (менеджер/клиент/сотрудник).
|
||||
try:
|
||||
await bot.set_chat_menu_button(
|
||||
menu_button=MenuButtonWebApp(
|
||||
text="ЗОВ",
|
||||
web_app=WebAppInfo(url=config.miniapp_url),
|
||||
),
|
||||
)
|
||||
logging.info("Установлена меню-кнопка MiniApp: %s", config.miniapp_url)
|
||||
except Exception as e:
|
||||
logging.warning("Не удалось установить меню-кнопку: %s", e)
|
||||
|
||||
logging.info("Запуск в режиме polling")
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
await dp.start_polling(bot)
|
||||
|
||||
@ -346,6 +346,70 @@ function buildMenu(items) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
/* ----------------- Role chooser — первый экран MiniApp ----------------- */
|
||||
function renderRoleChooser() {
|
||||
app.innerHTML = "";
|
||||
document.body.classList.remove("has-bottom-nav");
|
||||
const oldNav = document.getElementById("bottom-nav");
|
||||
if (oldNav) oldNav.remove();
|
||||
|
||||
app.appendChild(el(`
|
||||
<div class="role-chooser">
|
||||
<div class="role-chooser-head">
|
||||
<div class="kicker">ЗОВ — кухня и техника</div>
|
||||
<h1 class="display-title">Кто <span class="accent">вы?</span></h1>
|
||||
<p class="lede">Выберите роль — кабинет откроется одним тапом.</p>
|
||||
</div>
|
||||
<div class="role-cards">
|
||||
<button class="role-card" data-role="manager">
|
||||
<div class="role-icon">👤</div>
|
||||
<div class="role-text">
|
||||
<div class="role-title">Я менеджер</div>
|
||||
<div class="role-sub">Веду клиентов и заказы</div>
|
||||
</div>
|
||||
<div class="role-arrow">${ICONS.chevron || "›"}</div>
|
||||
</button>
|
||||
<button class="role-card" data-role="client">
|
||||
<div class="role-icon">🏠</div>
|
||||
<div class="role-text">
|
||||
<div class="role-title">Я клиент</div>
|
||||
<div class="role-sub">Заказал кухню ЗОВ</div>
|
||||
</div>
|
||||
<div class="role-arrow">${ICONS.chevron || "›"}</div>
|
||||
</button>
|
||||
<button class="role-card" data-role="staff">
|
||||
<div class="role-icon">🔧</div>
|
||||
<div class="role-text">
|
||||
<div class="role-title">Я сотрудник</div>
|
||||
<div class="role-sub">Замерщик или сборщик ЗОВ</div>
|
||||
</div>
|
||||
<div class="role-arrow">${ICONS.chevron || "›"}</div>
|
||||
</button>
|
||||
</div>
|
||||
<p class="muted" style="text-align:center;margin-top:24px;font-size:12px;">
|
||||
Свой выбор можно изменить позже в профиле.
|
||||
</p>
|
||||
</div>
|
||||
`));
|
||||
app.querySelectorAll(".role-card").forEach(card => {
|
||||
card.addEventListener("click", () => {
|
||||
const role = card.dataset.role;
|
||||
haptic && haptic("impact");
|
||||
// Меняем URL и перезапускаем init() — fetchMe пойдёт с правильной ролью
|
||||
const qp = new URLSearchParams(window.location.search);
|
||||
qp.set("role", role);
|
||||
history.replaceState(null, "", `?${qp.toString()}${location.hash || ""}`);
|
||||
// Показываем splash снова — на время загрузки
|
||||
const splashEl = document.createElement("div");
|
||||
splashEl.id = "splash";
|
||||
splashEl.className = "loader splash";
|
||||
splashEl.innerHTML = `<div class="loader-bar"></div><div class="loader-caption">Открываем кабинет</div>`;
|
||||
document.body.appendChild(splashEl);
|
||||
init();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------- Staff (замерщик / сборщик) ----------------- */
|
||||
async function renderStaff(me) {
|
||||
app.innerHTML = "";
|
||||
@ -658,11 +722,8 @@ function hideSplash() {
|
||||
|
||||
async function init() {
|
||||
setupTelegram();
|
||||
// Hash-роутер: позволяет открывать подэкраны (например подбор) напрямую
|
||||
window.addEventListener("hashchange", routeByHash);
|
||||
|
||||
// ?go=podbor|clients|measure|request — бот может задать стартовый экран через query,
|
||||
// потому что Telegram WebApp не передаёт hash через KeyboardButton.web_app.
|
||||
const qp = new URLSearchParams(window.location.search);
|
||||
const goScreen = qp.get("go");
|
||||
if (goScreen && !location.hash) {
|
||||
@ -673,11 +734,18 @@ async function init() {
|
||||
request: "#/request",
|
||||
};
|
||||
if (map[goScreen]) {
|
||||
// Меняем hash без триггера hashchange (init сам отрендерит правильный экран)
|
||||
history.replaceState(null, "", location.pathname + location.search + map[goScreen]);
|
||||
}
|
||||
}
|
||||
|
||||
// Если нет ?role= в URL — показываем выбор роли (универсально для всех клиентов)
|
||||
const explicitRole = qp.get("role");
|
||||
if (!explicitRole && !location.hash) {
|
||||
renderRoleChooser();
|
||||
hideSplash();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const me = await fetchMe();
|
||||
window.__zovMe = me; // кешируем профиль для подэкранов
|
||||
|
||||
@ -1989,6 +1989,91 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ===== Role chooser — первый экран ===== */
|
||||
.role-chooser {
|
||||
padding: 32px 18px;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
min-height: calc(100vh - 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.role-chooser-head {
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.role-chooser-head .kicker {
|
||||
font-family: var(--font-mono, "JetBrains Mono", monospace);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted, #998877);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.role-chooser-head .display-title {
|
||||
font-size: 36px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.role-chooser-head .lede {
|
||||
font-size: 14px;
|
||||
color: var(--ink-2, #6B5C4A);
|
||||
margin: 0;
|
||||
}
|
||||
.role-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.role-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 18px 18px;
|
||||
background: var(--card, #fff);
|
||||
border: 1px solid var(--line-strong, rgba(15, 15, 14, 0.16));
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
.role-card:active {
|
||||
background: var(--paper-2, #F5EDDC);
|
||||
transform: scale(0.985);
|
||||
}
|
||||
.role-card .role-icon {
|
||||
font-size: 32px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--warm, rgba(107, 74, 43, 0.08));
|
||||
border-radius: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.role-card .role-text { flex: 1; min-width: 0; }
|
||||
.role-card .role-title {
|
||||
font-family: var(--font-display, "Newsreader", serif);
|
||||
font-style: italic;
|
||||
font-size: 20px;
|
||||
color: var(--ink, #1F1A14);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.role-card .role-sub {
|
||||
font-size: 12.5px;
|
||||
color: var(--muted, #998877);
|
||||
font-family: var(--font-mono, "JetBrains Mono", monospace);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.role-card .role-arrow {
|
||||
color: var(--muted, #998877);
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== Замер: фото с тегами ===== */
|
||||
.podbor-header .podbor-help {
|
||||
background: transparent;
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260513m">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260513m">
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260513n">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260513n">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Splash — за пределами #app, render-функции его не смывают -->
|
||||
@ -34,13 +34,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<main id="app"></main>
|
||||
<script src="assets/icons.js?v=20260513m"></script>
|
||||
<script src="assets/podbor.config.js?v=20260513m"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260513m"></script>
|
||||
<script src="assets/podbor.js?v=20260513m"></script>
|
||||
<script src="assets/clients.js?v=20260513m"></script>
|
||||
<script src="assets/measurements.js?v=20260513m"></script>
|
||||
<script src="assets/request.js?v=20260513m"></script>
|
||||
<script src="assets/app.js?v=20260513m"></script>
|
||||
<script src="assets/icons.js?v=20260513n"></script>
|
||||
<script src="assets/podbor.config.js?v=20260513n"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260513n"></script>
|
||||
<script src="assets/podbor.js?v=20260513n"></script>
|
||||
<script src="assets/clients.js?v=20260513n"></script>
|
||||
<script src="assets/measurements.js?v=20260513n"></script>
|
||||
<script src="assets/request.js?v=20260513n"></script>
|
||||
<script src="assets/app.js?v=20260513n"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user