This commit is contained in:
j3d1 2026-08-17 20:11:33 +02:00
parent 95ddb484eb
commit 8622e488a4
16 changed files with 607 additions and 369 deletions

View file

@ -0,0 +1,138 @@
<template>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Label layout</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-3">
<div v-for="t in labelTemplates" :key="t.id" class="template-option text-center"
:class="{'template-option-disabled': !isAvailable(t)}"
:title="isAvailable(t) ? '' : 'Not available - open this page from an item to fill in the fields this layout needs.'"
role="button" @click="isAvailable(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>
<div class="small mt-1"
:class="{'fw-bold text-primary': value === t.id}">
{{ t.name }}
</div>
<div class="small text-muted">{{ t.description }}</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.template-option {
width: 20rem;
}
.template-option-disabled {
opacity: .45;
cursor: not-allowed;
}
.template-thumb-canvas {
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. */
object-fit: contain;
background: #fff;
}
</style>
<script>
import {LABEL_TEMPLATES, drawFallbackLabel} from "@/label-drawing.js";
export default {
name: "LabelLayoutPreview",
props: {
// Named content fields the templates draw from (see label-content.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.
fields: {
type: Object,
required: true
},
// The selected template id.
value: {
type: String,
required: true
}
},
model: {
prop: "value",
event: "input"
},
emits: ["input"],
computed: {
labelTemplates() {
return LABEL_TEMPLATES;
}
},
watch: {
fields() {
this.redraw();
}
},
methods: {
setTemplateCanvasRef(id, el) {
if (el) {
this.templateCanvases[id] = el;
} else {
delete this.templateCanvases[id];
}
},
isAvailable(t) {
return [t.qr, t.text].filter(Boolean).every(key => this.fields[key] !== undefined);
},
contentFor(t) {
return {
qr: t.qr ? this.fields[t.qr] : null,
text: t.text ? this.fields[t.text] : null,
};
},
/* 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. */
redraw() {
for (const t of LABEL_TEMPLATES) {
const canvas = this.templateCanvases[t.id];
if (!canvas) {
continue;
}
const content = this.contentFor(t);
if (!this.isAvailable(t) || (!content.qr && !content.text)) {
canvas.width = 1;
canvas.height = 1;
continue;
}
try {
drawFallbackLabel(canvas, content);
} catch (e) {
// A thumbnail that can't render at this content length just stays blank
// rather than surfacing an error for every keystroke.
}
}
}
},
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.
this.templateCanvases = {};
},
mounted() {
this.redraw();
}
}
</script>