Relocate warranty, measuringtools, network, printers, usb, notifications, computers, and slides into plugins/<name>/frontend/. Each plugin's views are pulled from wherever they lived (own dir, plus the shared views/settings/, views/reports/, views/print/ dirs, and top-level views) into the plugin's frontend/views/, and its route file becomes the self-contained routes.js. Handled the messy cases: - computers: name mismatch (its views live in views/pcs/) - moved by following the route file's own imports, so the dir name did not matter. Its OS/access- protocol/PC-type settings views move with it (only computers.js routed them). - network: NetworkHub's sibling sub-views (NetworkDevicesList, SubnetsBrowse, not directly routed) moved too so its `./` imports resolve. - printers: the qrLogo helper is SHARED with core AssetLabel, so it stays in views/print/ and PrinterQR imports it via @/views/print/qrLogo. - slides: route file is toplevel-only (TVDashboard); SlideManager stays core (core.js routes /settings/slides). frontend/src/views/ now holds only core views; frontend/src/router/routes/ holds only core.js. All 13 plugins are self-contained under plugins/<name>/frontend/. Verified live: Network (hub + moved sub-views), Computers (name mismatch), GE-Enforce (helper), printedparts all render from their staged frontends. Build + 58 vitest + naming green.
188 lines
6.0 KiB
Vue
188 lines
6.0 KiB
Vue
<template>
|
|
<div>
|
|
<div class="no-print">
|
|
<button class="print-btn" :disabled="!printer" @click="print">Print QR Code</button>
|
|
<label>Position:
|
|
<select class="position-select" v-model="position">
|
|
<option value="1">1 - Top Left</option>
|
|
<option value="2">2 - Top Right</option>
|
|
<option value="3">3 - Middle Left</option>
|
|
<option value="4">4 - Middle Right</option>
|
|
<option value="5">5 - Bottom Left</option>
|
|
<option value="6">6 - Bottom Right</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading-msg">Loading...</div>
|
|
|
|
<div v-else-if="printer" class="print-sheet">
|
|
<div
|
|
v-for="pos in 6"
|
|
:key="pos"
|
|
class="label"
|
|
:class="[`pos-${pos}`, pos === parseInt(position) ? 'active' : 'inactive']"
|
|
>
|
|
<template v-if="pos === parseInt(position)">
|
|
<div class="model-name">{{ printer.printer?.modelname || '' }}</div>
|
|
<div class="qr-container">
|
|
<img v-if="qrImage" :src="qrImage" class="qr-img" alt="QR" />
|
|
</div>
|
|
<div class="info-section">
|
|
<div class="csf-name">{{ printer.assetnumber }}</div>
|
|
<div class="info-inner">
|
|
<div v-if="printer.printer?.windowsname" class="info-row">{{ printer.printer.windowsname }}</div>
|
|
<div v-if="ipAddress" class="info-row">{{ ipAddress }}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="error-msg">Printer not found</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { printersApi } from '@/api'
|
|
import { renderQrDataUrl } from '@/views/print/qrLogo'
|
|
import { buildQrUrl } from '@/utils/qrTarget'
|
|
|
|
const route = useRoute()
|
|
const loading = ref(true)
|
|
const printer = ref(null)
|
|
const position = ref('1')
|
|
// Render QR to a data-URL image, not a live <canvas>: canvases are unreliable
|
|
// in print output, images print every time.
|
|
const qrImage = ref('')
|
|
|
|
const ipAddress = computed(() => {
|
|
// Check direct ipaddress field first (from list API)
|
|
if (printer.value?.ipaddress && printer.value.ipaddress !== 'USB') return printer.value.ipaddress
|
|
// Fall back to communications array (from detail API)
|
|
if (!printer.value?.communications?.length) return null
|
|
const primary = printer.value.communications.find(c => c.isprimary) || printer.value.communications[0]
|
|
return primary?.ipaddress || primary?.address || null
|
|
})
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const response = await printersApi.get(route.params.id)
|
|
printer.value = response.data.data
|
|
} catch (error) {
|
|
console.error('Error loading printer:', error)
|
|
} finally {
|
|
loading.value = false
|
|
await nextTick()
|
|
generateQR()
|
|
}
|
|
})
|
|
|
|
watch(position, async () => {
|
|
await nextTick()
|
|
generateQR()
|
|
})
|
|
|
|
async function generateQR() {
|
|
if (!printer.value) return
|
|
const p = printer.value
|
|
const detailId = p.printer?.printerid || p.assetid
|
|
const qrUrl = await buildQrUrl('qr_target_printer', `/printers/${detailId}`, {
|
|
printerid: p.printer?.printerid || '',
|
|
assetid: p.assetid || '',
|
|
assetnumber: p.assetnumber || '',
|
|
serialnumber: p.serialnumber || '',
|
|
ip: ipAddress.value || '',
|
|
hostname: p.printer?.windowsname || p.name || '',
|
|
})
|
|
qrImage.value = await renderQrDataUrl(qrUrl)
|
|
}
|
|
|
|
function print() {
|
|
window.print()
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
@page { size: letter; margin: 0; }
|
|
|
|
.no-print { margin-bottom: 20px; text-align: center; padding: 20px; }
|
|
|
|
.print-btn {
|
|
padding: 10px 30px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
margin: 5px;
|
|
}
|
|
.print-btn:hover:not(:disabled) { background: var(--primary-dark); }
|
|
.print-btn:disabled { background: var(--text-light); cursor: not-allowed; }
|
|
|
|
.position-select { padding: 8px; font-size: 14px; margin-left: 10px; }
|
|
|
|
.loading-msg, .error-msg {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
font-size: 1.125rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.print-sheet {
|
|
width: 8.5in;
|
|
height: 11in;
|
|
background: white;
|
|
margin: 0 auto;
|
|
position: relative;
|
|
border: 1px solid #ccc;
|
|
}
|
|
|
|
.label {
|
|
width: 3in;
|
|
height: 3in;
|
|
position: absolute;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0.1in;
|
|
box-sizing: border-box;
|
|
}
|
|
.label.inactive { border: 1px dashed #ccc; }
|
|
.label.active { border: 2px solid var(--primary); }
|
|
|
|
.qr-img { width: 144px; height: 144px; display: block; }
|
|
|
|
.pos-1 { top: 0.875in; left: 1.1875in; }
|
|
.pos-2 { top: 0.875in; left: 4.3125in; }
|
|
.pos-3 { top: 4in; left: 1.1875in; }
|
|
.pos-4 { top: 4in; left: 4.3125in; }
|
|
.pos-5 { top: 7.125in; left: 1.1875in; }
|
|
.pos-6 { top: 7.125in; left: 4.3125in; }
|
|
|
|
.model-name { font-size: 11pt; font-weight: bold; text-align: center; margin-bottom: 0.1in; color: #000; }
|
|
.qr-container { text-align: center; }
|
|
.info-section { margin-top: 0.1in; display: flex; flex-direction: column; align-items: center; }
|
|
.info-inner { text-align: left; }
|
|
.info-row { font-size: 9pt; color: #333; margin: 1px 0; white-space: nowrap; }
|
|
.csf-name { font-size: 12pt; font-weight: bold; font-family: monospace; text-align: center; margin-bottom: 2px; color: #000; }
|
|
|
|
@media print {
|
|
/* Force rendered images/colors to print even when "Background graphics" is
|
|
off, otherwise the QR image can drop out. */
|
|
body, .print-sheet, .label, .qr-img, .qr-container {
|
|
-webkit-print-color-adjust: exact !important;
|
|
print-color-adjust: exact !important;
|
|
}
|
|
body { padding: 0; margin: 0; background: white; }
|
|
.no-print { display: none !important; }
|
|
.print-sheet { border: none; margin: 0; width: 8.5in; height: 11in; overflow: hidden; }
|
|
.label { border: none !important; }
|
|
.label.inactive { visibility: hidden; }
|
|
}
|
|
</style>
|