fix(scanner): Robuste QR-Erkennung mit jsQR (Kamera & Foto-Upload)
All checks were successful
Build and Deploy / build (push) Successful in 56s
All checks were successful
Build and Deploy / build (push) Successful in 56s
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
|
||||
<!-- HTML5-QRCode Scanner Library -->
|
||||
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
|
||||
<!-- jsQR & Html5Qrcode für maximale Kompatibilität -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #090b10;
|
||||
@@ -162,15 +162,48 @@
|
||||
.notification.success { display: block; background: rgba(16, 185, 129, 0.2); border: 1px solid var(--success); color: #a7f3d0; }
|
||||
.notification.error { display: block; background: rgba(239, 68, 68, 0.2); border: 1px solid var(--danger); color: #fecaca; }
|
||||
|
||||
#reader {
|
||||
.scanner-box {
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
max-width: 500px;
|
||||
margin: 16px auto;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
position: relative;
|
||||
background: #000;
|
||||
border: 2px solid #8b5cf6;
|
||||
}
|
||||
#video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
.scan-guide {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
border: 2px dashed #8b5cf6;
|
||||
border-radius: 12px;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 0 0 4000px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.scan-line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #ec4899;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
box-shadow: 0 0 8px #ec4899;
|
||||
animation: scanAnim 2s infinite linear;
|
||||
}
|
||||
@keyframes scanAnim {
|
||||
0% { top: 0; }
|
||||
50% { top: 100%; }
|
||||
100% { top: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -213,18 +246,29 @@
|
||||
|
||||
<!-- QR-Code Scanner / Setup -->
|
||||
<div class="card" style="border-left: 4px solid #8b5cf6;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<h3 style="color: #a78bfa;">📷 Live QR-Code Scanner (Kamera & JSON)</h3>
|
||||
<button type="button" id="btn-toggle-camera" class="btn btn-qr" style="padding: 8px 14px; font-size: 13px;">
|
||||
🎥 Kamera öffnen
|
||||
</button>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 10px;">
|
||||
<h3 style="color: #a78bfa;">📷 QR-Code Setup (Kamera / Bild / JSON)</h3>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<label class="btn btn-secondary" style="margin: 0; padding: 8px 14px; font-size: 13px; cursor: pointer;">
|
||||
📁 Foto hochladen <input type="file" id="qr-file-input" accept="image/*" style="display: none;" />
|
||||
</label>
|
||||
<button type="button" id="btn-toggle-camera" class="btn btn-qr" style="padding: 8px 14px; font-size: 13px;">
|
||||
🎥 Kamera starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px;">
|
||||
Halte den POS Cloud Einrichtungs-QR-Code vor die Kamera oder füge das JSON unten manuell ein.
|
||||
QR-Code vor die Kamera halten, als Screenshot hochladen oder JSON direkt einfügen.
|
||||
</p>
|
||||
|
||||
<!-- Kamera Viewport -->
|
||||
<div id="reader"></div>
|
||||
<div id="scanner-box" class="scanner-box">
|
||||
<video id="video" playsinline></video>
|
||||
<canvas id="canvas" style="display: none;"></canvas>
|
||||
<div class="scan-guide">
|
||||
<div class="scan-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="qr-form" style="margin-top: 12px;">
|
||||
<div class="form-group">
|
||||
@@ -323,8 +367,8 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let html5QrCode = null;
|
||||
let cameraActive = false;
|
||||
let videoStream = null;
|
||||
let scanAnimationId = null;
|
||||
|
||||
function showAlert(id, msg, isError = false) {
|
||||
const el = document.getElementById(id);
|
||||
@@ -401,9 +445,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Kamera Scanner Toggle
|
||||
document.getElementById('btn-toggle-camera').addEventListener('click', async () => {
|
||||
if (cameraActive) {
|
||||
// ==================== Robuster jsQR Scanner ====================
|
||||
|
||||
document.getElementById('btn-toggle-camera').addEventListener('click', () => {
|
||||
if (videoStream) {
|
||||
stopCamera();
|
||||
} else {
|
||||
startCamera();
|
||||
@@ -411,50 +456,99 @@
|
||||
});
|
||||
|
||||
async function startCamera() {
|
||||
const readerEl = document.getElementById('reader');
|
||||
const scannerBox = document.getElementById('scanner-box');
|
||||
const video = document.getElementById('video');
|
||||
const btn = document.getElementById('btn-toggle-camera');
|
||||
readerEl.style.display = 'block';
|
||||
|
||||
scannerBox.style.display = 'block';
|
||||
btn.textContent = '⏹️ Kamera stoppen';
|
||||
btn.style.background = '#ef4444';
|
||||
|
||||
html5QrCode = new Html5Qrcode("reader");
|
||||
cameraActive = true;
|
||||
|
||||
try {
|
||||
await html5QrCode.start(
|
||||
{ facingMode: "environment" },
|
||||
{ fps: 10, qrbox: { width: 250, height: 250 } },
|
||||
(decodedText) => {
|
||||
// QR Code erkannt
|
||||
document.getElementById('qr-json').value = decodedText;
|
||||
stopCamera();
|
||||
submitQrPayload(decodedText);
|
||||
},
|
||||
(errorMessage) => {}
|
||||
);
|
||||
videoStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
|
||||
});
|
||||
video.srcObject = videoStream;
|
||||
video.setAttribute('playsinline', true);
|
||||
await video.play();
|
||||
scanAnimationId = requestAnimationFrame(scanTick);
|
||||
} catch (err) {
|
||||
showAlert('dash-alert', 'Kamera konnte nicht gestartet werden: ' + err, true);
|
||||
showAlert('dash-alert', 'Kamerazugriff verweigert oder HTTPS fehlt: ' + err.message, true);
|
||||
stopCamera();
|
||||
}
|
||||
}
|
||||
|
||||
async function stopCamera() {
|
||||
if (html5QrCode && cameraActive) {
|
||||
try {
|
||||
await html5QrCode.stop();
|
||||
html5QrCode.clear();
|
||||
} catch(e) {}
|
||||
function scanTick() {
|
||||
const video = document.getElementById('video');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
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: 'attemptBoth'
|
||||
});
|
||||
|
||||
if (code && code.data) {
|
||||
console.log('QR Code erkannt:', code.data);
|
||||
document.getElementById('qr-json').value = code.data;
|
||||
stopCamera();
|
||||
submitQrPayload(code.data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cameraActive = false;
|
||||
const readerEl = document.getElementById('reader');
|
||||
if (readerEl) readerEl.style.display = 'none';
|
||||
|
||||
if (videoStream) {
|
||||
scanAnimationId = requestAnimationFrame(scanTick);
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
if (scanAnimationId) cancelAnimationFrame(scanAnimationId);
|
||||
if (videoStream) {
|
||||
videoStream.getTracks().forEach(track => track.stop());
|
||||
videoStream = null;
|
||||
}
|
||||
document.getElementById('scanner-box').style.display = 'none';
|
||||
const btn = document.getElementById('btn-toggle-camera');
|
||||
if (btn) {
|
||||
btn.textContent = '🎥 Kamera öffnen';
|
||||
btn.textContent = '🎥 Kamera starten';
|
||||
btn.style.background = '#8b5cf6';
|
||||
}
|
||||
}
|
||||
|
||||
// QR Foto Upload
|
||||
document.getElementById('qr-file-input').addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: 'attemptBoth'
|
||||
});
|
||||
|
||||
if (code && code.data) {
|
||||
document.getElementById('qr-json').value = code.data;
|
||||
submitQrPayload(code.data);
|
||||
} else {
|
||||
showAlert('dash-alert', 'Kein QR-Code im Bild gefunden. Bitte JSON manuell einfügen.', true);
|
||||
}
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
|
||||
async function submitQrPayload(qrContent) {
|
||||
showAlert('dash-alert', 'QR-Code Handshake läuft...');
|
||||
try {
|
||||
@@ -465,7 +559,7 @@
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert('dash-alert', 'QR-Code Handshake erfolgreich! Kasse & ServicePass aktiviert.');
|
||||
showAlert('dash-alert', 'QR-Code Handshake erfolgreich! Parameter übernommen & aktiviert.');
|
||||
loadConfig();
|
||||
loadData();
|
||||
} else {
|
||||
@@ -476,7 +570,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// QR Handshake Form Submit
|
||||
document.getElementById('qr-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const qrContent = document.getElementById('qr-json').value;
|
||||
|
||||
Reference in New Issue
Block a user