Виправлення помилок.
Додан новий метод авторізації в застосунок.
This commit is contained in:
+298
@@ -0,0 +1,298 @@
|
||||
<!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="Закрити">×</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>
|
||||
Reference in New Issue
Block a user