mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 18:24:49 +00:00
feat: platform.js — адаптер Telegram WebApp для миграции на VK Max
- platform.js: глобальный Platform (initData, initDataUnsafe, startParam, colorScheme, ready, expand, haptic, showAlert, onThemeChange, enableClosingConfirmation) - tg остаётся глобальным для backward-совместимости модулей - app.js: setupTelegram() и haptic() делегируют в Platform, все tg?.initData/initDataUnsafe → Platform.initData/initDataUnsafe - index.html: platform.js грузится первым (перед icons.js) - VK Max Phase 2: заменить platform.js, остальной код не трогать Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cb4bbfce70
commit
ef51ebcb85
@ -1,8 +1,7 @@
|
|||||||
// ЗОВ MiniApp — главный скрипт. v20260518l
|
// ЗОВ MiniApp — главный скрипт. v20260518n
|
||||||
// На входе: подписанный initData от Telegram.
|
// На входе: подписанный initData от Telegram.
|
||||||
// Ходим на backend → получаем профиль (роль, статус) → рендерим меню.
|
// Ходим на backend → получаем профиль (роль, статус) → рендерим меню.
|
||||||
|
// tg и Platform определены в platform.js (загружается первым).
|
||||||
const tg = window.Telegram?.WebApp;
|
|
||||||
// Cloudflare Quick Tunnel → VPS FastAPI backend (GigaChat).
|
// Cloudflare Quick Tunnel → VPS FastAPI backend (GigaChat).
|
||||||
// Временный URL — пока wasrusgen1.pro в verification-hold; затем переключим на https://api.wasrusgen1.pro
|
// Временный URL — пока wasrusgen1.pro в verification-hold; затем переключим на https://api.wasrusgen1.pro
|
||||||
// Позволяет переключить бэкенд через ?backend=https://staging.api.wasrusgen1.pro
|
// Позволяет переключить бэкенд через ?backend=https://staging.api.wasrusgen1.pro
|
||||||
@ -34,31 +33,21 @@ function savedVariant() {
|
|||||||
try { return localStorage.getItem(THEME_KEY) ?? ""; } catch(e) { return ""; }
|
try { return localStorage.getItem(THEME_KEY) ?? ""; } catch(e) { return ""; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------- Telegram WebApp setup ----------------- */
|
/* ----------------- Platform setup ----------------- */
|
||||||
function setupTelegram() {
|
function setupTelegram() {
|
||||||
const scheme = tg?.colorScheme || (window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
document.documentElement.setAttribute("data-theme", Platform.colorScheme);
|
||||||
document.documentElement.setAttribute("data-theme", scheme);
|
|
||||||
// Восстанавливаем тему из localStorage (по умолч. — brand)
|
|
||||||
applyVariant(savedVariant());
|
applyVariant(savedVariant());
|
||||||
|
|
||||||
if (!tg) return;
|
Platform.ready();
|
||||||
try {
|
Platform.expand();
|
||||||
tg.ready();
|
Platform.onThemeChange(() => {
|
||||||
tg.expand();
|
document.documentElement.setAttribute("data-theme", Platform.colorScheme);
|
||||||
if (tg.onEvent) tg.onEvent("themeChanged", () => {
|
|
||||||
document.documentElement.setAttribute("data-theme", tg.colorScheme || "light");
|
|
||||||
});
|
});
|
||||||
if (tg.enableClosingConfirmation) tg.enableClosingConfirmation();
|
Platform.enableClosingConfirmation();
|
||||||
} catch (e) { console.warn(e); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function haptic(type = "selection") {
|
function haptic(type = "selection") {
|
||||||
try {
|
Platform.haptic(type);
|
||||||
if (!tg?.HapticFeedback) return;
|
|
||||||
if (type === "impact") tg.HapticFeedback.impactOccurred("light");
|
|
||||||
else if (type === "success") tg.HapticFeedback.notificationOccurred("success");
|
|
||||||
else tg.HapticFeedback.selectionChanged();
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------- Palette switcher UI ----------------- */
|
/* ----------------- Palette switcher UI ----------------- */
|
||||||
@ -119,11 +108,11 @@ async function fetchMe() {
|
|||||||
const res = await fetch(`${BACKEND_URL}/api/me`, {
|
const res = await fetch(`${BACKEND_URL}/api/me`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
initData: tg?.initData || "",
|
initData: Platform.initData,
|
||||||
// Fallback для Telegram Desktop side-panel где initData может приходить пустым.
|
// Fallback для Telegram Desktop side-panel где initData может приходить пустым.
|
||||||
// Backend проверит подпись initData первым; если её нет — упадёт сюда. UNSAFE!
|
// Backend проверит подпись initData первым; если её нет — упадёт сюда. UNSAFE!
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: Platform.initDataUnsafe,
|
||||||
startParam: tg?.initDataUnsafe?.start_param || null,
|
startParam: Platform.startParam,
|
||||||
role: explicitRole,
|
role: explicitRole,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -219,7 +208,7 @@ async function renderManagerHome(me) {
|
|||||||
card.addEventListener("click", () => {
|
card.addEventListener("click", () => {
|
||||||
haptic("impact");
|
haptic("impact");
|
||||||
if (qa.href) location.hash = qa.href;
|
if (qa.href) location.hash = qa.href;
|
||||||
else tg?.showAlert?.(`«${qa.title}» — скоро`);
|
else Platform.showAlert(`«${qa.title}» — скоро`);
|
||||||
});
|
});
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
@ -246,7 +235,7 @@ async function renderManagerHome(me) {
|
|||||||
// Параллельно грузим реальные данные (измерения + pending — критичные)
|
// Параллельно грузим реальные данные (измерения + pending — критичные)
|
||||||
// Складские данные грузим отдельно, чтобы ошибка Drive не ломала весь дашборд
|
// Складские данные грузим отдельно, чтобы ошибка Drive не ломала весь дашборд
|
||||||
try {
|
try {
|
||||||
const authBody = { initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null };
|
const authBody = { initData: Platform.initData, initDataUnsafe: Platform.initDataUnsafe };
|
||||||
const [resM, resP] = await Promise.all([
|
const [resM, resP] = await Promise.all([
|
||||||
fetch(`${BACKEND_URL}/api/measurements`, { method: "POST", body: JSON.stringify(authBody) }),
|
fetch(`${BACKEND_URL}/api/measurements`, { method: "POST", body: JSON.stringify(authBody) }),
|
||||||
fetch(`${BACKEND_URL}/api/manager_pending`, { method: "POST", body: JSON.stringify(authBody) }),
|
fetch(`${BACKEND_URL}/api/manager_pending`, { method: "POST", body: JSON.stringify(authBody) }),
|
||||||
@ -319,8 +308,8 @@ async function handlePodborDecision(item, act, card) {
|
|||||||
const res = await fetch(`${BACKEND_URL}/api/measurement_decision`, {
|
const res = await fetch(`${BACKEND_URL}/api/measurement_decision`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
initData: tg?.initData || "",
|
initData: Platform.initData,
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: Platform.initDataUnsafe,
|
||||||
measurement_id: item.id,
|
measurement_id: item.id,
|
||||||
decision,
|
decision,
|
||||||
}),
|
}),
|
||||||
@ -639,7 +628,7 @@ function renderBottomNav(active, opts = {}) {
|
|||||||
`);
|
`);
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
haptic("impact");
|
haptic("impact");
|
||||||
if (t.key !== active) tg?.showAlert?.(`«${t.label || "Новое"}» — скоро`);
|
if (t.key !== active) Platform.showAlert(`«${t.label || "Новое"}» — скоро`);
|
||||||
});
|
});
|
||||||
nav.appendChild(btn);
|
nav.appendChild(btn);
|
||||||
});
|
});
|
||||||
@ -868,7 +857,7 @@ async function renderStaff(me) {
|
|||||||
const t1 = setTimeout(() => ctrl1.abort(), 15000);
|
const t1 = setTimeout(() => ctrl1.abort(), 15000);
|
||||||
const res = await fetch(`${BACKEND_URL}/api/measurement_inbox`, {
|
const res = await fetch(`${BACKEND_URL}/api/measurement_inbox`, {
|
||||||
method: "POST", signal: ctrl1.signal,
|
method: "POST", signal: ctrl1.signal,
|
||||||
body: JSON.stringify({ initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null }),
|
body: JSON.stringify({ initData: Platform.initData, initDataUnsafe: Platform.initDataUnsafe }),
|
||||||
});
|
});
|
||||||
clearTimeout(t1);
|
clearTimeout(t1);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -928,7 +917,7 @@ async function renderStaffAssemblies(container) {
|
|||||||
const t2 = setTimeout(() => ctrl2.abort(), 15000);
|
const t2 = setTimeout(() => ctrl2.abort(), 15000);
|
||||||
const res = await fetch(`${BACKEND_URL}/api/assembly_list`, {
|
const res = await fetch(`${BACKEND_URL}/api/assembly_list`, {
|
||||||
method: "POST", signal: ctrl2.signal,
|
method: "POST", signal: ctrl2.signal,
|
||||||
body: JSON.stringify({ initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null }),
|
body: JSON.stringify({ initData: Platform.initData, initDataUnsafe: Platform.initDataUnsafe }),
|
||||||
});
|
});
|
||||||
clearTimeout(t2);
|
clearTimeout(t2);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -1213,7 +1202,7 @@ async function renderInboxDetail(measurementId) {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/measurement_detail`, {
|
const res = await fetch(`${BACKEND_URL}/api/measurement_detail`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ initData: tg?.initData || "", measurement_id: measurementId }),
|
body: JSON.stringify({ initData: Platform.initData, measurement_id: measurementId }),
|
||||||
});
|
});
|
||||||
m = await res.json();
|
m = await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -1343,8 +1332,8 @@ async function saveScheduleDate(measurementId, section) {
|
|||||||
const res = await fetch(`${BACKEND_URL}/api/measurement_schedule`, {
|
const res = await fetch(`${BACKEND_URL}/api/measurement_schedule`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
initData: tg?.initData || "",
|
initData: Platform.initData,
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: Platform.initDataUnsafe,
|
||||||
measurement_id: measurementId,
|
measurement_id: measurementId,
|
||||||
scheduled_at: iso,
|
scheduled_at: iso,
|
||||||
}),
|
}),
|
||||||
@ -1355,7 +1344,7 @@ async function saveScheduleDate(measurementId, section) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
haptic && haptic("success");
|
haptic && haptic("success");
|
||||||
tg?.showAlert?.("Дата сохранена — менеджер уведомлён.");
|
Platform.showAlert("Дата сохранена — менеджер уведомлён.");
|
||||||
renderInboxDetail(measurementId); // перерисовать с новым статусом
|
renderInboxDetail(measurementId); // перерисовать с новым статусом
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (errorEl) errorEl.textContent = "Сеть: " + e.message;
|
if (errorEl) errorEl.textContent = "Сеть: " + e.message;
|
||||||
@ -1513,8 +1502,8 @@ function renderLogisticsBlock(m) {
|
|||||||
const res = await fetch(`${BACKEND_URL}/api/geocode`, {
|
const res = await fetch(`${BACKEND_URL}/api/geocode`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
initData: tg?.initData || "",
|
initData: Platform.initData,
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: Platform.initDataUnsafe,
|
||||||
address: addr,
|
address: addr,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -1549,8 +1538,8 @@ function renderLogisticsBlock(m) {
|
|||||||
}
|
}
|
||||||
const parkType = (section.querySelector('input[name="parkType"]:checked') || {}).value || "";
|
const parkType = (section.querySelector('input[name="parkType"]:checked') || {}).value || "";
|
||||||
const payload = {
|
const payload = {
|
||||||
initData: tg?.initData || "",
|
initData: Platform.initData,
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: Platform.initDataUnsafe,
|
||||||
measurement_id: m.id,
|
measurement_id: m.id,
|
||||||
entrance: section.querySelector("#logEntrance").value,
|
entrance: section.querySelector("#logEntrance").value,
|
||||||
floor: section.querySelector("#logFloor").value,
|
floor: section.querySelector("#logFloor").value,
|
||||||
|
|||||||
69
miniapp/assets/platform.js
Normal file
69
miniapp/assets/platform.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/* ============================================================
|
||||||
|
platform.js — адаптер платформы
|
||||||
|
Telegram Phase 1 · VK Max Phase 2 (отдельно, не параллельно)
|
||||||
|
|
||||||
|
Загружается ПЕРВЫМ из всех app-скриптов (после telegram-web-app.js).
|
||||||
|
Определяет два глобальных:
|
||||||
|
tg — ссылка на WebApp (backward-совместимость модулей)
|
||||||
|
Platform — единый API без привязки к Telegram SDK
|
||||||
|
|
||||||
|
Миграция на VK Max: заменить этот файл, остальной код не трогать.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* global */ var tg = window.Telegram?.WebApp || null; // eslint-disable-line no-var
|
||||||
|
|
||||||
|
const Platform = (function () {
|
||||||
|
"use strict";
|
||||||
|
const _tg = tg;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ── Auth ────────────────────────────────────────────────
|
||||||
|
/** Подписанная строка initData — для HMAC-верификации на бэкенде */
|
||||||
|
get initData() { return _tg?.initData || ""; },
|
||||||
|
/** Небезопасный объект (fallback для Telegram Desktop) */
|
||||||
|
get initDataUnsafe() { return _tg?.initDataUnsafe || null; },
|
||||||
|
/** Параметр ?startapp= / start_param от бота */
|
||||||
|
get startParam() { return _tg?.initDataUnsafe?.start_param || null; },
|
||||||
|
|
||||||
|
// ── Тема ────────────────────────────────────────────────
|
||||||
|
/** "light" | "dark" — берём из платформы или matchMedia */
|
||||||
|
get colorScheme() {
|
||||||
|
return _tg?.colorScheme
|
||||||
|
|| (window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Lifecycle ───────────────────────────────────────────
|
||||||
|
/** Сигнал платформе: MiniApp готов к показу */
|
||||||
|
ready() { try { _tg?.ready?.(); } catch (e) { /* не в Telegram → ок */ } },
|
||||||
|
/** Развернуть на весь экран */
|
||||||
|
expand() { try { _tg?.expand?.(); } catch (e) {} },
|
||||||
|
/** Подтверждение при закрытии (предотвращает случайный свайп) */
|
||||||
|
enableClosingConfirmation() { try { _tg?.enableClosingConfirmation?.(); } catch (e) {} },
|
||||||
|
|
||||||
|
// ── События ─────────────────────────────────────────────
|
||||||
|
/** Подписка на смену темы платформой */
|
||||||
|
onThemeChange(cb) { try { _tg?.onEvent?.("themeChanged", cb); } catch (e) {} },
|
||||||
|
|
||||||
|
// ── UI ──────────────────────────────────────────────────
|
||||||
|
/** Нативный alert платформы (fallback → window.alert) */
|
||||||
|
showAlert(msg) {
|
||||||
|
if (_tg?.showAlert) { try { _tg.showAlert(msg); return; } catch (e) {} }
|
||||||
|
alert(msg);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Haptic ──────────────────────────────────────────────
|
||||||
|
/**
|
||||||
|
* Тактильный отклик.
|
||||||
|
* @param {"impact"|"success"|"selection"} type
|
||||||
|
*/
|
||||||
|
haptic(type = "selection") {
|
||||||
|
try {
|
||||||
|
const hf = _tg?.HapticFeedback;
|
||||||
|
if (!hf) return;
|
||||||
|
if (type === "impact") hf.impactOccurred("light");
|
||||||
|
else if (type === "success") hf.notificationOccurred("success");
|
||||||
|
else hf.selectionChanged();
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -35,6 +35,7 @@
|
|||||||
<div class="brand-tagline-gold">CRM</div>
|
<div class="brand-tagline-gold">CRM</div>
|
||||||
</div>
|
</div>
|
||||||
<main id="app"></main>
|
<main id="app"></main>
|
||||||
|
<script src="assets/platform.js?v=20260518n"></script>
|
||||||
<script src="assets/icons.js?v=20260516h"></script>
|
<script src="assets/icons.js?v=20260516h"></script>
|
||||||
<script src="assets/podbor.config.js?v=20260516h"></script>
|
<script src="assets/podbor.config.js?v=20260516h"></script>
|
||||||
<script src="assets/podbor.picts.js?v=20260516h"></script>
|
<script src="assets/podbor.picts.js?v=20260516h"></script>
|
||||||
@ -51,6 +52,6 @@
|
|||||||
<script src="assets/selfmeasure.js?v=20260518k"></script>
|
<script src="assets/selfmeasure.js?v=20260518k"></script>
|
||||||
<script src="assets/orders.js?v=20260518l"></script>
|
<script src="assets/orders.js?v=20260518l"></script>
|
||||||
<script src="assets/assembly_detail.js?v=20260518m"></script>
|
<script src="assets/assembly_detail.js?v=20260518m"></script>
|
||||||
<script src="assets/app.js?v=20260518m"></script>
|
<script src="assets/app.js?v=20260518n"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user