LabelPrinter Implementation

This commit is contained in:
2026-01-06 11:58:29 +01:00
parent c1ed8cd028
commit d3fc2e6ad3
6 changed files with 460 additions and 2 deletions

View File

@@ -0,0 +1,130 @@
<script setup lang="ts">
import LabelPrinterButton from "~/components/LabelPrinterButton.vue";
const labelPrinter = useLabelPrinterStore()
const props = defineProps({
context: {
type: Object,
required: true,
default: {}
}
})
defineShortcuts({
meta_p: {
usingInput: true,
handler: () => {
if(!labelPrinter.connected) return
printLabel()
}
},
escape: {
usingInput: true,
handler: () => {
modal.close()
}
}
})
const emit = defineEmits(["printed"])
const modal = useModal()
const { $api } = useNuxtApp()
const loading = ref(true)
const printing = ref(false)
const labelData = ref<any>(null) // gerendertes Bild vom Backend
/** Label vom Backend rendern */
async function loadLabel() {
loading.value = true
try {
labelData.value = await $api(`/api/print/label`, {
method: "POST",
body: JSON.stringify({
context: props.context || null
})
})
} catch (err) {
console.error("Label render error", err)
}
loading.value = false
}
/** Drucken */
async function printLabel() {
if (!labelPrinter.connected) return
printing.value = true
try {
await labelPrinter.print(labelData.value.encoded, { density: 5, pages: 1 })
modal.close()
} catch (err) {
console.error("Print error", err)
}
printing.value = false
}
const handleConnect = async () => {
await loadLabel()
await labelPrinter.connect("ble")
}
onMounted(() => {
if(labelPrinter.connected) {
loadLabel()
}
})
</script>
<template>
<UModal>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">Label drucken</h3>
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="modal.close()" />
</div>
</template>
<div v-if="!loading && labelPrinter.connected">
<img
:src="`data:image/png;base64,${labelData.base64}`"
alt="Label Preview"
class="max-w-full max-h-64 object-contain"
/>
</div>
<div v-else-if="loading && !labelPrinter.connected">
Kein Drucker verbunden
<LabelPrinterButton/>
</div>
<UProgress animation="carousel" v-else/>
<template #footer>
<div class="flex justify-between">
<UButton variant="ghost" @click="modal.close()">Abbrechen</UButton>
<UButton
color="primary"
:disabled="!labelPrinter.connected || printing"
:loading="printing"
@click="printLabel"
>
Drucken
<UKbd></UKbd>
<UKbd>P</UKbd>
</UButton>
</div>
</template>
</UCard>
</UModal>
</template>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
const labelPrinter = useLabelPrinterStore()
const showPrinterInfo = ref(false)
const handleClick = async () => {
if(labelPrinter.connected) {
showPrinterInfo.value = true
} else {
await labelPrinter.connect('ble')
}
}
</script>
<template>
<!-- Printer Button -->
<UModal v-model="showPrinterInfo">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">Drucker Informationen</h3>
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="showPrinterInfo = false" />
</div>
</template>
<p>Seriennummer: {{labelPrinter.info.serial}}</p>
<p>MAC: {{labelPrinter.info.mac}}</p>
<p>Modell: {{labelPrinter.info.modelId}}</p>
<p>Charge: {{labelPrinter.info.charge}}</p>
<p>Hardware Version: {{labelPrinter.info.hardwareVersion}}</p>
<p>Software Version: {{labelPrinter.info.softwareVersion}}</p>
</UCard>
</UModal>
<UButton
:icon="labelPrinter.connected ? 'i-heroicons-printer' : 'i-heroicons-printer'"
:color="labelPrinter.connected ? 'green' : 'gray'"
variant="soft"
class="w-full justify-start"
:loading="labelPrinter.connectLoading"
@click="handleClick"
>
<span v-if="labelPrinter.connected">Drucker verbunden</span>
<span v-else>Drucker verbinden</span>
</UButton>
</template>
<style scoped>
</style>

60
components/nimbot.vue Normal file
View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
const labelPrinter = useLabelPrinterStore()
const { $api } = useNuxtApp()
const labelWidth = ref(50)
const labelHeight = ref(30)
const zpl = ref(`^XA
^FO5,5
^GB545,325,3^FS
^CF0,30
^FO20,20^FDHello from Bluetooth!^FS
^FO20,100^BY2
^BXN,10,200
^FD12345678901234567^FS
^XZ`)
async function handlePrint() {
const dpmm = 12
const res = await $api("/api/print/label", {
method: "POST",
body: JSON.stringify({
zpl: zpl.value,
width: labelWidth.value,
height: labelHeight.value,
dpmm,
}),
})
await labelPrinter.print(res, { density: 5, pages: 1 })
}
</script>
<template>
<UCard class="p-4">
<div class="flex gap-2 mb-2">
<UButton @click="labelPrinter.connect('serial')">Connect S </UButton>
<UButton @click="labelPrinter.connect('ble')">Connect B</UButton>
<UButton color="red" @click="labelPrinter.disconnect">Disconnect</UButton>
<UButton color="primary" @click="handlePrint" :disabled="!labelPrinter.connected">Print</UButton>
</div>
{{labelPrinter.info}}
{{labelPrinter.printProgress}}
<UFormGroup label="Breite">
<UInput v-model="labelWidth"><template #trailing>mm</template></UInput>
</UFormGroup>
<UFormGroup label="Höhe">
<UInput v-model="labelHeight"><template #trailing>mm</template></UInput>
</UFormGroup>
<UFormGroup label="ZPL">
<UTextarea v-model="zpl" rows="6" />
</UFormGroup>
</UCard>
</template>

View File

@@ -6,6 +6,7 @@ import dayjs from "dayjs";
import {useCapacitor} from "../composables/useCapacitor.js";
import GlobalMessages from "~/components/GlobalMessages.vue";
import TenantDropdown from "~/components/TenantDropdown.vue";
import LabelPrinterButton from "~/components/LabelPrinterButton.vue";
const dataStore = useDataStore()
const colorMode = useColorMode()
@@ -14,6 +15,7 @@ const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const labelPrinter = useLabelPrinterStore()
const month = dayjs().format("MM")
@@ -240,13 +242,16 @@ const footerLinks = [
<template #footer>
<div class="flex flex-col w-full">
<div class="flex flex-col gap-3 w-full">
<LabelPrinterButton/>
<!-- Footer Links -->
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div>
</template>
</UDashboardSidebar>
</UDashboardPanel>

View File

@@ -56,16 +56,36 @@
>
<display-open-tasks/>
</UDashboardCard>
<UDashboardCard
title="Label Test"
>
<UButton
@click="modal.open(LabelPrintModal, {
context: {
datamatrix: '1234',
text: 'FEDEO TEST'
}
})"
icon="i-heroicons-printer"
>
Label Drucken
</UButton>
</UDashboardCard>
</UPageGrid>
</UDashboardPanelContent>
</template>
<script setup>
import Nimbot from "~/components/nimbot.vue";
import LabelPrintModal from "~/components/LabelPrintModal.vue";
definePageMeta({
middleware: 'redirect-to-mobile-index'
})
const modal = useModal();
const { isNotificationsSlideoverOpen } = useDashboard()
</script>

190
stores/labelPrinter.ts Normal file
View File

@@ -0,0 +1,190 @@
import { defineStore } from "pinia"
import {
Utils,
RequestCommandId,
ResponseCommandId,
NiimbotBluetoothClient,
NiimbotSerialClient
} from "@mmote/niimbluelib"
import { useToast } from "#imports"
export const useLabelPrinterStore = defineStore("labelPrinter", {
state: () => ({
client: null as NiimbotBluetoothClient | NiimbotSerialClient | null,
connected: false,
connectLoading: false,
transportLastUsed: "",
printProgress: 0,
info: {} as any
}),
actions: {
/** Logging Helper */
logger(...args: any[]) {
console.debug("[Printer]", ...args)
},
/** --- Client erzeugen --- */
newClient(transport: "ble" | "serial" = "serial") {
const toast = useToast()
// alten Client trennen
if (this.client) {
try { this.client.disconnect() } catch {}
}
// neuen Client erzeugen
this.client =
transport === "ble"
? new NiimbotBluetoothClient()
: new NiimbotSerialClient()
/** Events registrieren */
this.client.on("printerinfofetched", (e) => {
console.log("printerInfoFetched")
console.log(e.info)
this.info = e.info
})
this.client.on("connect", () => {
this.connected = true
toast.add({ title: "Drucker verbunden" })
this.logger("connected")
})
this.client.on("disconnect", () => {
this.connected = false
toast.add({ title: "Drucker getrennt" })
this.logger("disconnected")
})
this.client.on("printprogress", (e) => {
if (e.pagePrintProgress) this.printProgress = e.pagePrintProgress
this.logger(
`Page ${e.page}/${e.pagesTotal}, Page print ${e.pagePrintProgress}%, Page feed ${e.pageFeedProgress}%`
)
})
return this.client
},
/** --- Verbinden --- */
async connect(transport: "ble" | "serial" = "serial") {
const toast = useToast()
this.connectLoading = true
this.newClient(transport)
try {
await this.client!.connect()
this.transportLastUsed = transport
this.connectLoading = false
} catch (err) {
console.error("[Printer] Connect failed:", err)
toast.add({ title: "Verbindung fehlgeschlagen", color: "red" })
this.connectLoading = false
}
},
/** --- Trennen --- */
async disconnect({ forget = false } = {}) {
const toast = useToast()
this.logger("Disconnect requested…")
if (!this.client) return
try {
// Timer stoppen
try {
if (this.client.heartbeatTimer) {
clearInterval(this.client.heartbeatTimer)
this.client.heartbeatTimer = null
}
if (this.client.abstraction?.statusPollTimer) {
clearInterval(this.client.abstraction.statusPollTimer)
this.client.abstraction.statusPollTimer = null
}
} catch {}
await this.client.disconnect?.()
// Serial-Port schließen
const port = (this.client as any).port
if (port) {
try {
if (port.readable) port.readable.cancel?.()
if (port.writable) await port.writable.abort?.()
await port.close?.()
if (forget && navigator.serial?.forgetPort) {
await navigator.serial.forgetPort(port)
}
} catch (err) {
this.logger("Error closing port:", err)
}
}
// BLE GATT
if (
this.client instanceof NiimbotBluetoothClient &&
this.client.device?.gatt?.connected
) {
try {
this.client.device.gatt.disconnect()
} catch {}
}
this.connected = false
this.client = null
toast.add({ title: "Drucker getrennt" })
} catch (err) {
console.error("[Printer] Disconnect error", err)
toast.add({ title: "Fehler beim Trennen", color: "red" })
}
},
/** Hilfsfunktion: EncodedImage reparieren */
reviveEncodedImage(encoded: any) {
if (!encoded?.rowsData) return encoded
for (const row of encoded.rowsData) {
if (row.rowData && !(row.rowData instanceof Uint8Array)) {
row.rowData = new Uint8Array(Object.values(row.rowData))
}
}
return encoded
},
/** --- Drucken --- */
async print(encoded: any, options?: { density?: number; pages?: number }) {
const toast = useToast()
if (!this.client) throw new Error("Kein Drucker verbunden")
const fixed = this.reviveEncodedImage(encoded)
const taskName = this.client.getPrintTaskType() ?? "B1"
const task = this.client.abstraction.newPrintTask(taskName, {
totalPages: options?.pages ?? 1,
statusPollIntervalMs: 100,
statusTimeoutMs: 8000,
density: options?.density ?? 5
})
try {
this.printProgress = 0
await task.printInit()
await task.printPage(fixed, options?.pages ?? 1)
await task.waitForFinished()
toast.add({ title: "Druck abgeschlossen" })
} catch (e) {
console.error("[Printer] print error", e)
toast.add({ title: "Druckfehler", color: "red" })
} finally {
await task.printEnd()
}
}
}
})