From 4aaff0b08d06432ac74ecf4cdb28c433ea10f542 Mon Sep 17 00:00:00 2001 From: vchuser Date: Tue, 28 Jul 2026 17:00:44 +0200 Subject: [PATCH] feat: Scan-Ansicht und Treffer-Rueckmeldung --- src/main.js | 41 ++++++-------- src/styles.css | 112 ++++++++++++++++++++++++++++++++++++++- src/ui/result-overlay.js | 33 ++++++++++++ src/ui/scan-view.js | 66 +++++++++++++++++++++++ 4 files changed, 224 insertions(+), 28 deletions(-) create mode 100644 src/ui/result-overlay.js create mode 100644 src/ui/scan-view.js diff --git a/src/main.js b/src/main.js index d16c5f6..4c74c47 100644 --- a/src/main.js +++ b/src/main.js @@ -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 = ` - - -

-`;
-
-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));
diff --git a/src/styles.css b/src/styles.css
index f7bb793..5807a9d 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -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; }
diff --git a/src/ui/result-overlay.js b/src/ui/result-overlay.js
new file mode 100644
index 0000000..a58d167
--- /dev/null
+++ b/src/ui/result-overlay.js
@@ -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 = `
+    
STAPEL ${stackId}
+
+
+ `; + overlay.querySelector('.detail').textContent = beschreibung(spec); + overlay.querySelector('.pn').textContent = spec.partNumber ?? ''; + root.appendChild(overlay); + + return new Promise((resolve) => { + setTimeout(() => { + overlay.remove(); + resolve(); + }, dauerMs); + }); +} diff --git a/src/ui/scan-view.js b/src/ui/scan-view.js new file mode 100644 index 0000000..5cb229f --- /dev/null +++ b/src/ui/scan-view.js @@ -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 = ` +
+ +
+
+
+ +
+
+ Noch nichts erfasst + +
+ + `; + + 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(); + }, + }; +}