@@ -0,0 +1,285 @@
|
||||
(function () {
|
||||
const LOG = "[WespKioskPairing]";
|
||||
const API = {
|
||||
status: "/api/kiosk/status",
|
||||
accessLink: "/api/kiosk/access-link",
|
||||
setupAvailable: "/api/kiosk/setup-available",
|
||||
};
|
||||
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
||||
|
||||
function isLocalhostContext() {
|
||||
return LOCALHOST_HOSTS.has((window.location.hostname || "").toLowerCase());
|
||||
}
|
||||
|
||||
function ensureModalStyles() {
|
||||
const criticalId = "wespKioskPairCriticalCss";
|
||||
if (!document.getElementById(criticalId)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = criticalId;
|
||||
style.textContent =
|
||||
"#kioskPairModal{display:none;position:fixed;inset:0;z-index:5000;padding:8px;background:rgba(0,0,0,.55);overflow:hidden}" +
|
||||
"#kioskPairModal.kiosk-pair-open{display:flex;align-items:stretch;justify-content:center;height:100vh;height:100dvh}" +
|
||||
"body.kiosk-pair-modal-open{overflow:hidden!important}" +
|
||||
"#kioskPairModal .kiosk-pair-panel{display:flex;flex-direction:column;width:min(480px,100%);height:100%;max-height:100%;min-height:0;overflow:hidden;box-sizing:border-box}" +
|
||||
"#kioskPairModal .kiosk-pair-body{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}" +
|
||||
"#kioskPairModal .kiosk-pair-actions{flex:0 0 auto;display:flex;flex-wrap:wrap;gap:6px}";
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
if (document.getElementById("wespKioskModalsCss")) return;
|
||||
if (window.WespKioskTheme && typeof window.WespKioskTheme.ensureModalStyles === "function") {
|
||||
window.WespKioskTheme.ensureModalStyles();
|
||||
return;
|
||||
}
|
||||
const link = document.createElement("link");
|
||||
link.id = "wespKioskModalsCss";
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/static/css/kiosk-modals.css";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
function createModal() {
|
||||
ensureModalStyles();
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "kioskPairModal";
|
||||
modal.innerHTML = `
|
||||
<div class="kiosk-pair-panel" role="dialog" aria-modal="true" aria-labelledby="kioskPairTitle">
|
||||
<div class="kiosk-pair-header">
|
||||
<h3 id="kioskPairTitle">Привязать терминал</h3>
|
||||
<p class="kiosk-pair-subtitle">Start URL для Fully Kiosk Browser</p>
|
||||
</div>
|
||||
<div class="kiosk-pair-body">
|
||||
<div class="kiosk-pair-steps">
|
||||
1) Скопируйте ссылку или отсканируйте QR.<br/>
|
||||
2) Fully Kiosk → Settings → Start URL → вставьте ссылку → Save.<br/>
|
||||
3) Перезапустите Fully Kiosk.
|
||||
</div>
|
||||
<div id="kioskPairLoading" class="kiosk-pair-loading">
|
||||
<span class="kiosk-pair-spinner"></span>
|
||||
<span>Загрузка ссылки...</span>
|
||||
</div>
|
||||
<div class="kiosk-pair-qr-wrap">
|
||||
<img id="kioskPairQr" class="kiosk-pair-qr" alt="QR Start URL" />
|
||||
</div>
|
||||
<p id="kioskPairUrl" class="kiosk-pair-url"></p>
|
||||
<p id="kioskPairStatus" class="kiosk-pair-status"></p>
|
||||
</div>
|
||||
<div class="kiosk-pair-actions">
|
||||
<button id="kioskPairCopy" type="button" class="kiosk-pair-btn kiosk-pair-btn--primary">Скопировать ссылку</button>
|
||||
<button id="kioskPairRefresh" type="button" class="kiosk-pair-btn kiosk-pair-btn--secondary">Обновить ссылку</button>
|
||||
<button id="kioskPairClose" type="button" class="kiosk-pair-btn kiosk-pair-btn--ghost">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
return modal;
|
||||
}
|
||||
|
||||
function createBlockOverlay() {
|
||||
const el = document.createElement("div");
|
||||
el.id = "kioskBlockedOverlay";
|
||||
el.style.cssText =
|
||||
"display:none;position:fixed;inset:0;background:rgba(10,10,10,.78);z-index:4500;color:#fff;align-items:center;justify-content:center;text-align:center;padding:20px;";
|
||||
el.innerHTML = `
|
||||
<div style="max-width:520px">
|
||||
<h2 style="margin:0 0 10px 0">Терминал не привязан</h2>
|
||||
<p style="margin:0;line-height:1.4">Обратитесь к администратору для настройки терминала.</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
async function fetchJson(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
console.warn(LOG, url, response.status, data.message || data);
|
||||
throw new Error(data.message || `HTTP ${response.status}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function setup(opts) {
|
||||
const options = opts || {};
|
||||
const modal = createModal();
|
||||
const block = createBlockOverlay();
|
||||
const qrImg = modal.querySelector("#kioskPairQr");
|
||||
const qrWrap = modal.querySelector(".kiosk-pair-qr-wrap");
|
||||
const loadingEl = modal.querySelector("#kioskPairLoading");
|
||||
const urlEl = modal.querySelector("#kioskPairUrl");
|
||||
const statusEl = modal.querySelector("#kioskPairStatus");
|
||||
const copyBtn = modal.querySelector("#kioskPairCopy");
|
||||
const refreshBtn = modal.querySelector("#kioskPairRefresh");
|
||||
const closeBtn = modal.querySelector("#kioskPairClose");
|
||||
let currentStartUrl = "";
|
||||
let canIssueAccessLink = isLocalhostContext();
|
||||
|
||||
function isEnforcePairedOnly(data) {
|
||||
if (!data || typeof data !== "object") return true;
|
||||
return data.enforce_paired_only !== false;
|
||||
}
|
||||
|
||||
async function refreshSetupAvailability() {
|
||||
if (isLocalhostContext()) {
|
||||
canIssueAccessLink = true;
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const data = await fetchJson(API.setupAvailable);
|
||||
canIssueAccessLink = !!data.can_issue_access_link;
|
||||
} catch (e) {
|
||||
console.warn(LOG, "setup-available error", e);
|
||||
canIssueAccessLink = false;
|
||||
}
|
||||
if (options.menuLinkSelector) {
|
||||
const menuLink = document.querySelector(options.menuLinkSelector);
|
||||
if (menuLink) {
|
||||
menuLink.style.display = canIssueAccessLink ? "" : "none";
|
||||
}
|
||||
}
|
||||
return canIssueAccessLink;
|
||||
}
|
||||
|
||||
async function checkStatus() {
|
||||
if (isLocalhostContext()) {
|
||||
block.style.display = "none";
|
||||
if (typeof options.onPaired === "function") options.onPaired({ paired: true, localhost: true });
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const data = await fetchJson(API.status);
|
||||
const enforce = isEnforcePairedOnly(data);
|
||||
const paired = !!data.paired || !enforce;
|
||||
console.debug(LOG, "/api/kiosk/status", {
|
||||
paired: !!data.paired,
|
||||
enforce_paired_only: enforce,
|
||||
device_id: data.device_id,
|
||||
status: data.status,
|
||||
});
|
||||
block.style.display = paired ? "none" : "flex";
|
||||
if (paired && typeof options.onPaired === "function") options.onPaired(data);
|
||||
if (!paired && typeof options.onUnpaired === "function") options.onUnpaired(data);
|
||||
return paired;
|
||||
} catch (e) {
|
||||
console.warn(LOG, "checkStatus error", e);
|
||||
block.style.display = "flex";
|
||||
if (typeof options.onUnpaired === "function") options.onUnpaired({});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAccessLink(refresh) {
|
||||
if (!canIssueAccessLink) {
|
||||
statusEl.textContent = "Выдача Start URL доступна только администратору или с localhost.";
|
||||
statusEl.style.color = "#ff9f9f";
|
||||
return;
|
||||
}
|
||||
loadingEl.style.display = "flex";
|
||||
qrWrap.classList.remove("kiosk-pair-qr-visible");
|
||||
qrImg.removeAttribute("src");
|
||||
statusEl.textContent = refresh ? "Обновление ссылки..." : "Загрузка ссылки...";
|
||||
try {
|
||||
const data = await fetchJson(API.accessLink, {
|
||||
method: refresh ? "POST" : "GET",
|
||||
headers: refresh ? { "Content-Type": "application/json" } : undefined,
|
||||
body: refresh ? JSON.stringify({ refresh: true }) : undefined,
|
||||
});
|
||||
currentStartUrl = data.start_url || data.pair_url || "";
|
||||
console.info(LOG, "access-link OK", { start_url: currentStartUrl.slice(0, 80) });
|
||||
const qrSrc = data.qr_image_url || "";
|
||||
await new Promise((resolve) => {
|
||||
const done = () => {
|
||||
qrImg.onload = null;
|
||||
qrImg.onerror = null;
|
||||
resolve();
|
||||
};
|
||||
qrImg.onload = done;
|
||||
qrImg.onerror = done;
|
||||
qrImg.src = qrSrc;
|
||||
});
|
||||
loadingEl.style.display = "none";
|
||||
qrWrap.classList.add("kiosk-pair-qr-visible");
|
||||
qrImg.style.display = "block";
|
||||
urlEl.textContent = currentStartUrl;
|
||||
statusEl.textContent = "Постоянная ссылка. Вставьте её в Fully Kiosk → Start URL.";
|
||||
statusEl.style.color = "";
|
||||
} catch (e) {
|
||||
loadingEl.style.display = "none";
|
||||
statusEl.textContent = globalThis.WespUserMessages?.userFacingMessage?.(e.message, "Не удалось получить ссылку") || "Не удалось получить ссылку";
|
||||
statusEl.style.color = "#ff9f9f";
|
||||
}
|
||||
}
|
||||
|
||||
async function openModal() {
|
||||
if (!canIssueAccessLink) {
|
||||
window.alert("Выдача Start URL доступна только администратору или при открытии с localhost.");
|
||||
return;
|
||||
}
|
||||
modal.classList.add("kiosk-pair-open");
|
||||
document.body.classList.add("kiosk-pair-modal-open");
|
||||
loadAccessLink(false);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove("kiosk-pair-open");
|
||||
document.body.classList.remove("kiosk-pair-modal-open");
|
||||
}
|
||||
|
||||
async function copyStartUrl() {
|
||||
if (!currentStartUrl) {
|
||||
statusEl.textContent = "Сначала дождитесь загрузки ссылки";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(currentStartUrl);
|
||||
} else {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = currentStartUrl;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.left = "-9999px";
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
statusEl.textContent = "Ссылка скопирована";
|
||||
statusEl.style.color = "#9fd89f";
|
||||
} catch (e) {
|
||||
statusEl.textContent = "Не удалось скопировать — выделите ссылку вручную";
|
||||
statusEl.style.color = "#ff9f9f";
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshLink() {
|
||||
if (!window.confirm("Старая ссылка перестанет работать. Обновить Start URL?")) {
|
||||
return;
|
||||
}
|
||||
await loadAccessLink(true);
|
||||
}
|
||||
|
||||
copyBtn.addEventListener("click", copyStartUrl);
|
||||
refreshBtn.addEventListener("click", refreshLink);
|
||||
closeBtn.addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
|
||||
if (options.menuLinkSelector) {
|
||||
const menuLink = document.querySelector(options.menuLinkSelector);
|
||||
if (menuLink) {
|
||||
menuLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
openModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refreshSetupAvailability().then(function () {
|
||||
checkStatus();
|
||||
});
|
||||
return { checkStatus, openModal };
|
||||
}
|
||||
|
||||
window.WespKioskPairing = { setup };
|
||||
})();
|
||||
Reference in New Issue
Block a user