LabelPrinter Implementation
This commit is contained in:
130
components/LabelPrintModal.vue
Normal file
130
components/LabelPrintModal.vue
Normal 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>
|
||||||
53
components/LabelPrinterButton.vue
Normal file
53
components/LabelPrinterButton.vue
Normal 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
60
components/nimbot.vue
Normal 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>
|
||||||
@@ -6,6 +6,7 @@ import dayjs from "dayjs";
|
|||||||
import {useCapacitor} from "../composables/useCapacitor.js";
|
import {useCapacitor} from "../composables/useCapacitor.js";
|
||||||
import GlobalMessages from "~/components/GlobalMessages.vue";
|
import GlobalMessages from "~/components/GlobalMessages.vue";
|
||||||
import TenantDropdown from "~/components/TenantDropdown.vue";
|
import TenantDropdown from "~/components/TenantDropdown.vue";
|
||||||
|
import LabelPrinterButton from "~/components/LabelPrinterButton.vue";
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
@@ -14,6 +15,7 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const labelPrinter = useLabelPrinterStore()
|
||||||
|
|
||||||
|
|
||||||
const month = dayjs().format("MM")
|
const month = dayjs().format("MM")
|
||||||
@@ -240,13 +242,16 @@ const footerLinks = [
|
|||||||
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col gap-3 w-full">
|
||||||
|
|
||||||
|
<LabelPrinterButton/>
|
||||||
|
|
||||||
|
<!-- Footer Links -->
|
||||||
<UDashboardSidebarLinks :links="footerLinks" />
|
<UDashboardSidebarLinks :links="footerLinks" />
|
||||||
|
|
||||||
<UDivider class="sticky bottom-0" />
|
<UDivider class="sticky bottom-0" />
|
||||||
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
|
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</UDashboardSidebar>
|
</UDashboardSidebar>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
|
|||||||
@@ -56,16 +56,36 @@
|
|||||||
>
|
>
|
||||||
<display-open-tasks/>
|
<display-open-tasks/>
|
||||||
</UDashboardCard>
|
</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>
|
</UPageGrid>
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
|
import Nimbot from "~/components/nimbot.vue";
|
||||||
|
import LabelPrintModal from "~/components/LabelPrintModal.vue";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'redirect-to-mobile-index'
|
middleware: 'redirect-to-mobile-index'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
const { isNotificationsSlideoverOpen } = useDashboard()
|
const { isNotificationsSlideoverOpen } = useDashboard()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
190
stores/labelPrinter.ts
Normal file
190
stores/labelPrinter.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user