Files
rozenrod 9797ff6137 Виправлення помилок.
Додан новий метод авторізації в застосунок.
2026-06-16 16:50:01 +03:00

298 lines
8.9 KiB
HTML

<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Авторизація</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #121212;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
/* Контейнер авторизации */
#page-auth {
background: #1e1e1e;
padding: 30px;
border-radius: 16px;
border: 1px solid #2c2c2e;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
width: 100%;
max-width: 320px;
}
#page-auth-form div {
display: flex;
flex-direction: column;
gap: 12px;
}
input[type="password"] {
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
color: #fff;
padding: 12px;
border-radius: 8px;
font-size: 16px;
outline: none;
}
button[type="submit"], .btn-scan {
background-color: #007aff;
color: white;
border: none;
padding: 12px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button[type="submit"]:hover { background-color: #0056b3; }
.btn-scan {
background-color: #34c759;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-scan:hover { background-color: #248a3d; }
/* Стили для нативного <dialog> */
dialog {
background: #1e1e1e;
color: white;
border: 1px solid #2c2c2e;
border-radius: 20px;
padding: 20px;
width: 90%;
max-width: 360px;
margin: auto;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
/* Затемнение фона вокруг модалки */
dialog::backdrop {
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px); /* Легкое размытие заднего плана */
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.btn-close {
background: none;
border: none;
color: #8e8e93;
font-size: 24px;
cursor: pointer;
padding: 0 5px;
}
/* Контейнер камеры внутри модалки */
.scanner-container {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 12px;
overflow: hidden;
background-color: #000;
}
#video {
width: 100%;
height: 100%;
object-fit: cover;
}
.laser-box {
position: absolute;
top: 15%; left: 15%; width: 70%; height: 70%;
border: 2px solid rgba(52, 199, 89, 0.5);
border-radius: 12px;
pointer-events: none;
}
.laser-line {
position: absolute;
width: 100%; height: 2px;
background-color: #34c759;
box-shadow: 0 0 8px #34c759;
top: 0;
animation: scan 2s linear infinite;
}
@keyframes scan {
0% { top: 0%; }
50% { top: 100%; }
100% { top: 0%; }
}
#loading-status {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: #8e8e93;
text-align: center;
}
</style>
</head>
<body>
<div id="page-auth">
<form id="page-auth-form">
<div>
<input
type="password"
name="uuid"
id="auth-forms-uuid"
placeholder="UUID"
required=""
/>
<button type="button" id="open-scanner-btn" class="btn-scan">
<span>Сканувати QR-код</span>
</button>
<button type="submit">
<span>Авторизуватися</span>
</button>
</div>
</form>
</div>
<dialog id="scanner-dialog">
<div class="dialog-header">
<h3 style="margin:0; font-size:18px;">Сканування коду</h3>
<button class="btn-close" id="close-scanner-btn" aria-label="Закрити">&times;</button>
</div>
<div class="scanner-container">
<div id="loading-status">Запуск камери...</div>
<video id="video" autoplay playsinline></video>
<div class="laser-box">
<div class="laser-line"></div>
</div>
</div>
</dialog>
<canvas id="canvas" style="display: none;"></canvas>
<script>
const dialog = document.getElementById('scanner-dialog');
const openBtn = document.getElementById('open-scanner-btn');
const closeBtn = document.getElementById('close-scanner-btn');
const uuidInput = document.getElementById('auth-forms-uuid');
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const loadingStatus = document.getElementById('loading-status');
let streamRef = null;
let isScanning = false;
// --- Логика управления сканером ---
async function startCamera() {
loadingStatus.style.display = 'block';
loadingStatus.innerText = "Запуск камери...";
isScanning = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { exact: "environment" } }
});
streamRef = stream;
video.srcObject = stream;
video.setAttribute("playsinline", true);
video.play();
video.addEventListener('loadedmetadata', () => {
loadingStatus.style.display = 'none';
requestAnimationFrame(tick);
});
} catch (err) {
// Фолбек, если основная камера недоступна (например, на ПК)
try {
const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: true });
streamRef = fallbackStream;
video.srcObject = fallbackStream;
video.play();
loadingStatus.style.display = 'none';
requestAnimationFrame(tick);
} catch (fallbackErr) {
loadingStatus.innerText = "Помилка доступу до камери";
}
}
}
function stopCamera() {
isScanning = false;
if (streamRef) {
streamRef.getTracks().forEach(track => track.stop());
streamRef = null;
}
video.srcObject = null;
}
function tick() {
if (!isScanning) return;
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
if (code && code.data.trim() !== "") {
// Успешно считали UUID
uuidInput.value = code.data;
// Закрываем модалку (событие close сработает автоматически)
dialog.close();
return;
}
}
requestAnimationFrame(tick);
}
// --- Слушатели событий модального окна ---
// Открыть сканер
openBtn.addEventListener('click', () => {
dialog.showModal(); // Открывает как модальное окно (с бэкдропом)
startCamera();
});
// Закрыть сканер по кнопке крестика
closeBtn.addEventListener('click', () => {
dialog.close();
});
// Важно: событие 'close' срабатывает всегда (и при клике на крестик, и при нажатии Esc)
dialog.addEventListener('close', () => {
stopCamera();
});
</script>
</body>
</html>