This commit is contained in:
j3d1 2026-08-24 15:57:17 +02:00
parent 8d96bc97c4
commit ed04d98bf1
54 changed files with 661 additions and 1214 deletions

View file

@ -6,9 +6,9 @@
<div class="card-body">
<div class="template-grid d-flex flex-wrap align-items-start">
<div v-for="t in labelTemplates" :key="t.id" class="template-option d-flex flex-column text-center"
:class="{'template-option-disabled': !isAvailable(t)}"
:title="isAvailable(t) ? '' : 'Not available - fill in the fields this layout needs above.'"
role="button" @click="isAvailable(t) && $emit('input', t.id)">
:class="{'template-option-disabled': !isSelectable(t)}"
:title="unavailableReason(t)"
role="button" @click="isSelectable(t) && $emit('input', t.id)">
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
class="img-thumbnail template-thumb-canvas"
:class="{'border-primary': value === t.id}"></canvas>
@ -24,8 +24,7 @@
</template>
<style scoped>
/* This build pins Bootstrap 4 (no gap-* utilities, those are Bootstrap 5.1+), so the spacing
here is plain CSS gap rather than a Bootstrap gap-N class. */
/* This build pins Bootstrap 4 (no gap-* utilities, those are 5.1+) - hence plain CSS gap here. */
.template-grid {
gap: 1rem;
}
@ -43,9 +42,8 @@
display: block;
width: 100%;
height: 10rem;
/* The canvas itself is drawn at whatever size fits its content (see redraw/drawFallbackLabel)
- object-fit scales that down to the thumbnail box the same way it would for an <img>,
no manual zoom math needed. */
/* Canvas draws at whatever size fits its content (see redraw/drawFallbackLabel); object-fit
scales it into the thumbnail box like it would an <img>, no manual zoom math needed. */
object-fit: contain;
background: #fff;
}
@ -58,9 +56,9 @@ import {LABEL_TEMPLATES, templateIsAvailable, templateContent} from "@/label-lay
export default {
name: "LabelLayoutPreview",
props: {
// Named content fields the templates draw from (see label.js's buildLabelFields)
// - kept in sync by the parent, not owned here. A field missing from this object (rather
// than present-but-empty) means a template that needs it is unavailable right now.
// Named content fields templates draw from (see label.js's buildLabelFields), kept in
// sync by the parent. A field's absence (not just empty) means a template needing it is
// unavailable.
fields: {
type: Object,
required: true
@ -70,12 +68,11 @@ export default {
type: String,
required: true
},
// Print.vue's global qr/micro-qr/rmqr choice (see label.js's QR_CODE_TYPES) - passed
// straight through to drawFallbackLabel so these thumbnails match whatever symbology the
// main preview is actually using.
codeType: {
type: String,
default: "qr"
// Ids of the most-recently printed/downloaded templates, most recent first (see
// Print.vue's rememberPrintedTemplate); bubbled to the front of the grid below.
recentTemplateIds: {
type: Array,
default: () => []
}
},
model: {
@ -83,17 +80,29 @@ export default {
event: "input"
},
emits: ["input"],
data() {
return {
// Keyed by template id, holding the error message from its most recent redraw()
// failure (e.g. text too long, or too many QR modules for the thumbnail's fixed
// reference size - see label.js's snapQrToCrispSize). Absent, not just falsy, for a
// template that last drew fine, so `t.id in failed` matches "has a message".
failed: {},
};
},
computed: {
// LABEL_TEMPLATES with recentTemplateIds' entries pulled to the front (most recent
// first), everything else following in its original order.
labelTemplates() {
return LABEL_TEMPLATES;
const recent = this.recentTemplateIds
.map(id => LABEL_TEMPLATES.find(t => t.id === id))
.filter(Boolean);
const recentIds = new Set(recent.map(t => t.id));
return [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
}
},
watch: {
fields() {
this.redraw();
},
codeType() {
this.redraw();
}
},
methods: {
@ -109,10 +118,24 @@ export default {
return templateIsAvailable(t, this.fields);
},
/* Live per-template thumbnails. Always uses the content-fit fallback renderer (rather
than the tape-fed one), regardless of whether a real printer is connected - these are
illustrative previews sized by CSS object-fit, not the accurate to-be-printed canvas
the main preview is. */
// Greyed out (and unclickable) either because a needed field isn't filled in yet
// (isAvailable) or its content failed to render (see redraw()'s catch) - either way
// there's nothing a click could select that would actually show anything.
isSelectable(t) {
return this.isAvailable(t) && !(t.id in this.failed);
},
// The disabled thumbnail's tooltip - empty once it's selectable again.
unavailableReason(t) {
if (!this.isAvailable(t)) {
return "Not available - fill in the fields this layout needs above.";
}
return this.failed[t.id] ?? "";
},
// Live per-template thumbnails. Always uses the content-fit fallback renderer (not the
// tape-fed one) regardless of printer connection - illustrative previews via CSS
// object-fit, not the to-be-printed-accurate canvas the main preview is.
redraw() {
for (const t of LABEL_TEMPLATES) {
const canvas = this.templateCanvases[t.id];
@ -123,27 +146,32 @@ export default {
if (!this.isAvailable(t) || !content) {
canvas.width = 1;
canvas.height = 1;
delete this.failed[t.id];
continue;
}
try {
drawFallbackLabel(canvas, content, "along", this.codeType);
drawFallbackLabel(canvas, content, "along");
delete this.failed[t.id];
} catch (e) {
// A thumbnail that can't render at this content length just stays blank
// rather than surfacing an error for every keystroke.
// Grey the thumbnail out (see isSelectable/unavailableReason) rather than
// leaving it blank - selecting a template that can't render here would only
// hand Print.vue's own redraw the exact same failure.
canvas.width = 1;
canvas.height = 1;
this.failed[t.id] = e.message;
}
}
}
},
created() {
// Keyed by template id, populated by setTemplateCanvasRef() - a plain :ref="t.id" string
// inside v-for would still get Vue's refInFor array-collecting behavior even though
// each iteration uses a different name, turning this.$refs[t.id] into a one-element
// array rather than the canvas itself. A function ref sidesteps that entirely.
// in v-for still gets Vue's refInFor array-collecting behavior despite distinct names per
// iteration, turning this.$refs[t.id] into a one-element array; a function ref avoids that.
this.templateCanvases = {};
},
async mounted() {
// See Print.vue's mounted() - every thumbnail here has a "qrcode" leaf, so there's
// nothing worth drawing before this resolves.
// See Print.vue's mounted() - every thumbnail here has a qr/mqr/rmqr leaf, so nothing's
// worth drawing before the wasm resource resolves.
await preloadQrEncoder();
this.redraw();
}