feat: Scan-Ansicht und Treffer-Rueckmeldung
This commit is contained in:
+15
-26
@@ -1,29 +1,18 @@
|
||||
import { startCamera, grabFrame } from './camera.js';
|
||||
import { decodeBarcodes } from './barcode.js';
|
||||
import { renderScanView } from './ui/scan-view.js';
|
||||
import { showResult } from './ui/result-overlay.js';
|
||||
import { startCamera } from './camera.js';
|
||||
|
||||
const app = document.querySelector('#app');
|
||||
app.innerHTML = `
|
||||
<video id="v" style="width:100%"></video>
|
||||
<button id="b" style="min-height:56px;width:100%">Bild aufnehmen</button>
|
||||
<pre id="out"></pre>
|
||||
`;
|
||||
|
||||
const video = document.querySelector('#v');
|
||||
const out = document.querySelector('#out');
|
||||
|
||||
startCamera(video)
|
||||
.then(() => { out.textContent = 'Kamera laeuft'; })
|
||||
.catch((error) => { out.textContent = `Kamera nicht verfuegbar: ${error.message}`; });
|
||||
|
||||
document.querySelector('#b').addEventListener('click', async () => {
|
||||
try {
|
||||
// grabFrame wirft, wenn das Kamerabild noch nicht bereit ist (siehe
|
||||
// camera.js) - dieser Fall wird hier abgefangen und sichtbar gemacht,
|
||||
// statt unbehandelt zu verpuffen.
|
||||
const frame = grabFrame(video);
|
||||
const codes = await decodeBarcodes(frame);
|
||||
out.textContent = codes.length ? codes.join('\n') : 'kein Barcode gefunden';
|
||||
} catch (error) {
|
||||
out.textContent = `Fehler: ${error.message}`;
|
||||
}
|
||||
const view = renderScanView(app, {
|
||||
onCapture: () =>
|
||||
showResult(app, {
|
||||
stackId: 'B',
|
||||
spec: { capacityGb: 64, rank: '4DRx4', speed: 'PC4-2400', formFactor: 'LRDIMM', partNumber: 'M386A8K40BM1-CRC4Y' },
|
||||
confidence: 'green',
|
||||
}),
|
||||
onUndo: () => view.setLast('zurueckgenommen'),
|
||||
onOpenList: () => {},
|
||||
onPickFile: () => {},
|
||||
});
|
||||
view.setStacks([{ id: 'A', count: 12 }, { id: 'B', count: 4 }]);
|
||||
startCamera(view.video).catch(() => view.setStatus('Kamera nicht verfuegbar', true));
|
||||
|
||||
+110
-2
@@ -1,2 +1,110 @@
|
||||
:root { color-scheme: dark; }
|
||||
body { margin: 0; font-family: system-ui, sans-serif; }
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #14161a;
|
||||
--fg: #f2f4f7;
|
||||
--muted: #9aa3af;
|
||||
--green: #1f8a4c;
|
||||
--yellow: #b8860b;
|
||||
--red: #a02c2c;
|
||||
--line: #2a2e36;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
#app { display: flex; flex-direction: column; height: 100dvh; }
|
||||
|
||||
.camera { position: relative; flex: 1; overflow: hidden; background: #000; }
|
||||
.camera video { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.frame {
|
||||
position: absolute;
|
||||
inset: 20% 8%;
|
||||
border: 3px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stacks {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid var(--line);
|
||||
min-height: 56px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stacks button {
|
||||
flex: none;
|
||||
min-height: 44px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #1c2027;
|
||||
color: var(--fg);
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.last {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
min-height: 56px;
|
||||
font-size: 15px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.last span { flex: 1; }
|
||||
|
||||
.action {
|
||||
min-height: 56px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #2b6cb0;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.action.secondary { background: #2a2e36; }
|
||||
|
||||
.capture { margin: 12px; }
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.overlay.green { background: var(--green); }
|
||||
.overlay.yellow { background: var(--yellow); }
|
||||
.overlay.red { background: var(--red); }
|
||||
|
||||
.overlay .stack-id { font-size: 68px; font-weight: 800; letter-spacing: 2px; }
|
||||
.overlay .detail { font-size: 20px; line-height: 1.5; }
|
||||
.overlay .pn { font-size: 15px; opacity: 0.85; word-break: break-all; }
|
||||
|
||||
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
|
||||
.choices button { min-height: 64px; font-size: 20px; }
|
||||
|
||||
.status { padding: 8px 12px; font-size: 14px; color: var(--muted); }
|
||||
.status.warn { color: #f0b429; }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
function beschreibung(spec) {
|
||||
const teile = [
|
||||
spec.capacityGb ? `${spec.capacityGb}GB` : null,
|
||||
spec.rank,
|
||||
spec.speed,
|
||||
spec.formFactor,
|
||||
].filter(Boolean);
|
||||
return teile.join(' ') || 'ohne Angaben';
|
||||
}
|
||||
|
||||
/**
|
||||
* Blendet die Treffer-Rueckmeldung ein und nach kurzer Zeit wieder aus.
|
||||
* Gruen bei Barcode, gelb bei OCR. Keine Eingabe noetig.
|
||||
*/
|
||||
export function showResult(root, { stackId, spec, confidence }, dauerMs = 1200) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = `overlay ${confidence}`;
|
||||
overlay.innerHTML = `
|
||||
<div class="stack-id">STAPEL ${stackId}</div>
|
||||
<div class="detail"></div>
|
||||
<div class="pn"></div>
|
||||
`;
|
||||
overlay.querySelector('.detail').textContent = beschreibung(spec);
|
||||
overlay.querySelector('.pn').textContent = spec.partNumber ?? '';
|
||||
root.appendChild(overlay);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
overlay.remove();
|
||||
resolve();
|
||||
}, dauerMs);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Baut die Hauptansicht auf und liefert Aktualisierungsfunktionen zurueck.
|
||||
* Kennt weder Kamera noch Erkennung - alles kommt ueber die Rueckrufe.
|
||||
*/
|
||||
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile }) {
|
||||
root.innerHTML = `
|
||||
<div class="camera">
|
||||
<video id="preview" playsinline muted></video>
|
||||
<div class="frame"></div>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
<button class="action capture" id="capture">Modul scannen</button>
|
||||
<div class="stacks" id="stacks"></div>
|
||||
<div class="last">
|
||||
<span id="last">Noch nichts erfasst</span>
|
||||
<button class="action secondary" id="undo" style="min-width:64px">↶</button>
|
||||
</div>
|
||||
<input type="file" id="file" accept="image/*" hidden />
|
||||
`;
|
||||
|
||||
const stacksEl = root.querySelector('#stacks');
|
||||
const lastEl = root.querySelector('#last');
|
||||
const statusEl = root.querySelector('#status');
|
||||
const fileEl = root.querySelector('#file');
|
||||
|
||||
root.querySelector('#capture').addEventListener('click', onCapture);
|
||||
root.querySelector('#undo').addEventListener('click', onUndo);
|
||||
fileEl.addEventListener('change', () => {
|
||||
if (fileEl.files[0]) onPickFile(fileEl.files[0]);
|
||||
fileEl.value = '';
|
||||
});
|
||||
|
||||
return {
|
||||
video: root.querySelector('#preview'),
|
||||
|
||||
/** @param {{id: string, count: number}[]} stacks */
|
||||
setStacks(stacks) {
|
||||
stacksEl.innerHTML = '';
|
||||
if (stacks.length === 0) {
|
||||
stacksEl.textContent = 'Noch keine Stapel';
|
||||
return;
|
||||
}
|
||||
for (const stack of stacks) {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = `${stack.id}: ${stack.count}`;
|
||||
button.addEventListener('click', onOpenList);
|
||||
stacksEl.appendChild(button);
|
||||
}
|
||||
},
|
||||
|
||||
setLast(text) {
|
||||
lastEl.textContent = text;
|
||||
},
|
||||
|
||||
/** @param {string} text @param {boolean} warn */
|
||||
setStatus(text, warn = false) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.classList.toggle('warn', warn);
|
||||
},
|
||||
|
||||
openFilePicker() {
|
||||
fileEl.hidden = false;
|
||||
fileEl.click();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user