This commit is contained in:
j3d1 2026-08-19 23:09:21 +02:00
parent 4787acd8eb
commit 67c7415c3b
6 changed files with 691 additions and 407 deletions

View file

@ -12,6 +12,40 @@
alternatives below.
</div>
<div class="row mb-3">
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">Label content</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div :class="v === 'value' ? 'col-12' : 'col-md-6'" v-for="v in baseVars" :key="v">
<label class="form-label">{{ varLabel(v) }}</label>
<textarea v-if="v === 'value'" class="form-control" rows="3"
v-model="varValues[v]"
placeholder="https://example.com/…"></textarea>
<input v-else type="text" class="form-control" v-model="varValues[v]">
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">Calculated</h5>
</div>
<div class="card-body">
<div v-for="v in derivedVars" :key="v" class="mb-2">
<div class="small text-muted">{{ varLabel(v) }}</div>
<div class="text-break">{{ fields[v] || "—" }}</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="!usbSupported" class="row">
<div class="col-lg-7">
<div class="card">
@ -19,13 +53,7 @@
<h5 class="card-title mb-0">Label preview</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">QR code content</label>
<input type="text" class="form-control" v-model="value"
placeholder="https://example.com/…" autofocus>
</div>
<div class="label-preview mb-3" v-show="selectedContent.qr || selectedContent.text">
<div class="label-preview mb-3" v-show="selectedContent">
<canvas ref="fallbackCanvas"></canvas>
</div>
@ -67,13 +95,7 @@
Connect a printer to preview and print a label.
</p>
<template v-else>
<div class="mb-3">
<label class="form-label">QR code content</label>
<input type="text" class="form-control" v-model="value"
placeholder="https://example.com/…" autofocus>
</div>
<div class="label-preview mb-3" v-show="selectedContent.qr || selectedContent.text">
<div class="label-preview mb-3" v-show="selectedContent">
<canvas ref="labelCanvas"></canvas>
</div>
@ -149,8 +171,8 @@ import BaseLayout from "@/components/BaseLayout.vue";
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/weblabel.js";
import {buildLabelContent, buildLabelFields} from "@/label-content.js";
import {LABEL_TEMPLATES, tapeFromStatus, drawLabel, drawFallbackLabel, templateContent} from "@/label-drawing.js";
import {tapeFromStatus, drawLabel, drawFallbackLabel, buildLabelContent, buildLabelFields} from "@/label.js";
import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS, withDerivedVars, templateContent} from "@/label-layouts.js";
// Served verbatim from public/vendor/ rather than bundled: libweblabel.js's
// own emscripten glue resolves its .wasm sibling relative to *its own*
@ -189,7 +211,22 @@ export default {
tape: null,
labelBitmap: null,
value: buildLabelContent(this.prefill),
// One input per *base* template variable (see label-layouts.js's BASE_VARS) - the
// derived ones (itemUrl, itemHandle) are format strings calculated from these, not
// typed directly, so they're only ever shown (see the `fields` computed below), never
// stored here. Prefilled from the ?kind=& query params where
// buildLabelContent/buildLabelFields have a value for them, editable from there so a
// template needing e.g. userHandle isn't stuck depending on a prefill that never
// arrives.
varValues: {
...Object.fromEntries(BASE_VARS.map(v => [v, ""])),
value: buildLabelContent(this.prefill),
// Defaults to wherever this page itself is being served from - editable since any
// frontend can resolve any handle (see label-layouts.js's DERIVED_VARS.itemUrl),
// so a label doesn't have to point back at this particular one.
webdomain: window.location.origin,
...buildLabelFields(this.prefill),
},
copies: 1,
selectedTemplate: LABEL_TEMPLATES[0].id,
@ -200,11 +237,30 @@ export default {
};
},
computed: {
// Named content fields the field-specific templates draw from, plus the free-text
// `value` field the generic qr/qr-text/text templates use. A field this doesn't have
// (rather than one that's merely empty) is what LabelLayoutPreview.vue greys out.
// The base variables the "Label content" form renders an input for, and the derived ones
// it instead calculates and lists read-only beside that form - plain passthroughs, but
// keep the template from importing label-layouts.js just for these.
baseVars() {
return BASE_VARS;
},
derivedVars() {
return Object.keys(DERIVED_VARS);
},
// Named content fields the templates draw from: the form's own base vars, plus every
// DERIVED_VARS format string calculated live from those - so typing a userHandle and
// itemId (whether by hand or via prefill) recalculates itemUrl/itemHandle the same way
// either way. A blank/uncalculated value is dropped rather than passed through as an
// empty string, so it reads as *absent* to templateIsAvailable/templateContent the same
// way a prefill that never supplied it would - that's what LabelLayoutPreview.vue greys a
// template's thumbnail out on.
fields() {
return {value: this.value, ...buildLabelFields(this.prefill)};
const base = {};
for (const v of BASE_VARS) {
if (this.varValues[v]) {
base[v] = this.varValues[v];
}
}
return withDerivedVars(base);
},
currentTemplate() {
return LABEL_TEMPLATES.find(t => t.id === this.selectedTemplate) || LABEL_TEMPLATES[0];
@ -232,12 +288,15 @@ export default {
},
},
watch: {
value() {
if (this.usbSupported) {
this.redraw();
} else {
this.redrawFallback();
}
varValues: {
handler() {
if (this.usbSupported) {
this.redraw();
} else {
this.redrawFallback();
}
},
deep: true,
},
selectedTemplate() {
if (this.usbSupported) {
@ -258,6 +317,13 @@ export default {
},
},
methods: {
// Turns a camelCase variable name (see label-layouts.js's KNOWN_VARS) into a form label,
// e.g. "itemHandle" -> "Item Handle" - so adding a new template variable doesn't also
// require hand-writing a label for it here.
varLabel(v) {
return v.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, c => c.toUpperCase());
},
async guard(fn) {
this.error = null;
this.busy = true;
@ -326,7 +392,7 @@ export default {
redraw() {
this.labelBitmap = null;
const content = this.selectedContent;
if (!this.tape || (!content.qr && !content.text)) {
if (!this.tape || !content) {
return;
}
const canvas = this.$refs.labelCanvas;
@ -350,7 +416,7 @@ export default {
redrawFallback() {
this.fallbackReady = false;
const content = this.selectedContent;
if (!content.qr && !content.text) {
if (!content) {
return;
}
const canvas = this.$refs.fallbackCanvas;