fix(scanner): Robuste QR-Erkennung mit jsQR (Kamera & Foto-Upload)
All checks were successful
Build and Deploy / build (push) Successful in 56s

This commit is contained in:
DanielS
2026-09-22 22:18:17 +02:00
parent 204218c098
commit 35730c22c3

View File

@@ -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;