feat: Templates persistentes en servidor + Constructor con tabs por tipo de comando
- Templates ahora se guardan en servidor (data/templates.json) disponibles para todos - API CRUD para templates: GET/POST /api/templates, GET/PUT/DELETE /api/templates/[id] - Constructor de comandos rediseñado con tabs: Texto, Feed, Cortar, Pulse, QR, Barcode - Cada tipo de comando tiene su formulario específico con campos relevantes - Eliminado QuickActions (integrado en tabs del constructor) - Mejorada UI de lista de impresoras con renderizado condicional - Agregado data/ a .gitignore (datos de runtime)
This commit is contained in:
@@ -1,30 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
const queue = usePrintQueue()
|
||||
|
||||
// Estado del formulario
|
||||
// Tipo de comando activo
|
||||
const activeCommand = ref('text')
|
||||
|
||||
// Tipos de comandos disponibles
|
||||
const commandTypes = [
|
||||
{ label: 'Texto', value: 'text', icon: 'i-heroicons-document-text' },
|
||||
{ label: 'Feed', value: 'feed', icon: 'i-heroicons-arrow-down' },
|
||||
{ label: 'Cortar', value: 'cut', icon: 'i-heroicons-scissors' },
|
||||
{ label: 'Pulse', value: 'pulse', icon: 'i-heroicons-bolt' },
|
||||
{ label: 'QR', value: 'qr', icon: 'i-heroicons-qr-code' },
|
||||
{ label: 'Barcode', value: 'barcode', icon: 'i-heroicons-bars-3' }
|
||||
]
|
||||
|
||||
// ===== Estado para TEXTO =====
|
||||
const text = ref('')
|
||||
const align = ref('')
|
||||
const font = ref('')
|
||||
const width = ref<number | undefined>()
|
||||
const height = ref<number | undefined>()
|
||||
const align = ref('none')
|
||||
const font = ref('none')
|
||||
const textWidth = ref<number | undefined>()
|
||||
const textHeight = ref<number | undefined>()
|
||||
const bold = ref(false)
|
||||
const underline = ref(false)
|
||||
const reverse = ref(false)
|
||||
const smooth = ref(false)
|
||||
const color = ref('')
|
||||
const feedLines = ref<number | undefined>()
|
||||
const cut = ref('')
|
||||
const color = ref('none')
|
||||
|
||||
// Opciones para selects
|
||||
// ===== Estado para FEED =====
|
||||
const feedLines = ref(2)
|
||||
|
||||
// ===== Estado para CORTAR =====
|
||||
const cutType = ref('feed')
|
||||
|
||||
// ===== Estado para PULSE =====
|
||||
const pulseDrawer = ref('drawer_1')
|
||||
const pulseTime = ref('pulse_200')
|
||||
|
||||
// ===== Estado para QR =====
|
||||
const qrData = ref('')
|
||||
const qrModel = ref('qrcode_model_2')
|
||||
const qrLevel = ref('level_m')
|
||||
const qrSize = ref(6)
|
||||
|
||||
// ===== Estado para BARCODE =====
|
||||
const barcodeData = ref('')
|
||||
const barcodeType = ref('ean13')
|
||||
const barcodeHri = ref('below')
|
||||
const barcodeWidth = ref(3)
|
||||
const barcodeHeight = ref(80)
|
||||
|
||||
// ===== Opciones para selects =====
|
||||
const alignOptions = [
|
||||
{ label: '(sin cambio)', value: '' },
|
||||
{ label: '(sin cambio)', value: 'none' },
|
||||
{ label: 'Izquierda', value: 'left' },
|
||||
{ label: 'Centro', value: 'center' },
|
||||
{ label: 'Derecha', value: 'right' }
|
||||
]
|
||||
|
||||
const fontOptions = [
|
||||
{ label: '(sin cambio)', value: '' },
|
||||
{ label: '(sin cambio)', value: 'none' },
|
||||
{ label: 'Font A', value: 'font_a' },
|
||||
{ label: 'Font B', value: 'font_b' },
|
||||
{ label: 'Font C', value: 'font_c' },
|
||||
@@ -35,7 +69,7 @@ const fontOptions = [
|
||||
]
|
||||
|
||||
const colorOptions = [
|
||||
{ label: '(default)', value: '' },
|
||||
{ label: '(default)', value: 'none' },
|
||||
{ label: 'Color 1', value: 'color_1' },
|
||||
{ label: 'Color 2', value: 'color_2' },
|
||||
{ label: 'Color 3', value: 'color_3' },
|
||||
@@ -43,7 +77,6 @@ const colorOptions = [
|
||||
]
|
||||
|
||||
const cutOptions = [
|
||||
{ label: '(no cortar)', value: '' },
|
||||
{ label: 'Sin feed', value: 'no_feed' },
|
||||
{ label: 'Con feed', value: 'feed' },
|
||||
{ label: 'Reservar', value: 'reserve' },
|
||||
@@ -52,51 +85,173 @@ const cutOptions = [
|
||||
{ label: 'Full reservar', value: 'reserve_fullcut' }
|
||||
]
|
||||
|
||||
const formatSections = [
|
||||
{ label: 'Alineación y Fuente', value: 'alignment', icon: 'i-heroicons-bars-3-bottom-left' },
|
||||
{ label: 'Tamaño', value: 'size', icon: 'i-heroicons-arrows-pointing-out' },
|
||||
{ label: 'Estilo', value: 'style', icon: 'i-heroicons-paint-brush' },
|
||||
{ label: 'Acciones finales', value: 'actions', icon: 'i-heroicons-scissors' }
|
||||
const drawerOptions = [
|
||||
{ label: 'Drawer 1', value: 'drawer_1' },
|
||||
{ label: 'Drawer 2', value: 'drawer_2' }
|
||||
]
|
||||
|
||||
const pulseTimeOptions = [
|
||||
{ label: '100ms', value: 'pulse_100' },
|
||||
{ label: '200ms', value: 'pulse_200' },
|
||||
{ label: '300ms', value: 'pulse_300' },
|
||||
{ label: '400ms', value: 'pulse_400' },
|
||||
{ label: '500ms', value: 'pulse_500' }
|
||||
]
|
||||
|
||||
const qrModelOptions = [
|
||||
{ label: 'Model 1', value: 'qrcode_model_1' },
|
||||
{ label: 'Model 2', value: 'qrcode_model_2' }
|
||||
]
|
||||
|
||||
const qrLevelOptions = [
|
||||
{ label: 'Level L (7%)', value: 'level_l' },
|
||||
{ label: 'Level M (15%)', value: 'level_m' },
|
||||
{ label: 'Level Q (25%)', value: 'level_q' },
|
||||
{ label: 'Level H (30%)', value: 'level_h' }
|
||||
]
|
||||
|
||||
const barcodeTypeOptions = [
|
||||
{ label: 'UPC-A', value: 'upc_a' },
|
||||
{ label: 'UPC-E', value: 'upc_e' },
|
||||
{ label: 'EAN-13', value: 'ean13' },
|
||||
{ label: 'EAN-8', value: 'ean8' },
|
||||
{ label: 'Code 39', value: 'code39' },
|
||||
{ label: 'ITF', value: 'itf' },
|
||||
{ label: 'Codabar', value: 'codabar' },
|
||||
{ label: 'Code 93', value: 'code93' },
|
||||
{ label: 'Code 128', value: 'code128' },
|
||||
{ label: 'GS1-128', value: 'gs1_128' },
|
||||
{ label: 'GS1 DataBar', value: 'gs1_databar_omnidirectional' }
|
||||
]
|
||||
|
||||
const barcodeHriOptions = [
|
||||
{ label: 'Sin HRI', value: 'none' },
|
||||
{ label: 'Arriba', value: 'above' },
|
||||
{ label: 'Abajo', value: 'below' },
|
||||
{ label: 'Ambos', value: 'both' }
|
||||
]
|
||||
|
||||
// ===== Secciones de formato para texto =====
|
||||
const formatSections = [
|
||||
{ label: 'Alineación y Fuente', slot: 'alignment', icon: 'i-heroicons-bars-3-bottom-left' },
|
||||
{ label: 'Tamaño', slot: 'size', icon: 'i-heroicons-arrows-pointing-out' },
|
||||
{ label: 'Estilo', slot: 'style', icon: 'i-heroicons-paint-brush' }
|
||||
]
|
||||
|
||||
// ===== Funciones =====
|
||||
function resetForm() {
|
||||
// Reset texto
|
||||
text.value = ''
|
||||
align.value = ''
|
||||
font.value = ''
|
||||
width.value = undefined
|
||||
height.value = undefined
|
||||
align.value = 'none'
|
||||
font.value = 'none'
|
||||
textWidth.value = undefined
|
||||
textHeight.value = undefined
|
||||
bold.value = false
|
||||
underline.value = false
|
||||
reverse.value = false
|
||||
smooth.value = false
|
||||
color.value = ''
|
||||
feedLines.value = undefined
|
||||
cut.value = ''
|
||||
color.value = 'none'
|
||||
// Reset feed
|
||||
feedLines.value = 2
|
||||
// Reset cut
|
||||
cutType.value = 'feed'
|
||||
// Reset pulse
|
||||
pulseDrawer.value = 'drawer_1'
|
||||
pulseTime.value = 'pulse_200'
|
||||
// Reset QR
|
||||
qrData.value = ''
|
||||
qrModel.value = 'qrcode_model_2'
|
||||
qrLevel.value = 'level_m'
|
||||
qrSize.value = 6
|
||||
// Reset barcode
|
||||
barcodeData.value = ''
|
||||
barcodeType.value = 'ean13'
|
||||
barcodeHri.value = 'below'
|
||||
barcodeWidth.value = 3
|
||||
barcodeHeight.value = 80
|
||||
}
|
||||
|
||||
function queueText() {
|
||||
function addToQueue() {
|
||||
const ops: any[] = []
|
||||
|
||||
if (align.value) ops.push({ op: 'textAlign', align: align.value })
|
||||
if (font.value) ops.push({ op: 'textFont', font: font.value })
|
||||
if (width.value || height.value) {
|
||||
ops.push({ op: 'textSize', width: width.value, height: height.value })
|
||||
}
|
||||
ops.push({
|
||||
op: 'textStyle',
|
||||
em: bold.value,
|
||||
ul: underline.value,
|
||||
reverse: reverse.value,
|
||||
...(color.value ? { color: color.value } : {})
|
||||
})
|
||||
if (text.value) ops.push({ op: 'text', value: text.value })
|
||||
if (feedLines.value != null && feedLines.value !== 0) {
|
||||
ops.push({ op: 'feedLine', line: Number(feedLines.value) })
|
||||
}
|
||||
if (cut.value) ops.push({ op: 'cut', type: cut.value })
|
||||
switch (activeCommand.value) {
|
||||
case 'text':
|
||||
if (align.value && align.value !== 'none') ops.push({ op: 'textAlign', align: align.value })
|
||||
if (font.value && font.value !== 'none') ops.push({ op: 'textFont', font: font.value })
|
||||
if (textWidth.value || textHeight.value) {
|
||||
ops.push({ op: 'textSize', width: textWidth.value, height: textHeight.value })
|
||||
}
|
||||
ops.push({
|
||||
op: 'textStyle',
|
||||
em: bold.value,
|
||||
ul: underline.value,
|
||||
reverse: reverse.value,
|
||||
...(color.value && color.value !== 'none' ? { color: color.value } : {})
|
||||
})
|
||||
if (text.value) ops.push({ op: 'text', value: text.value })
|
||||
break
|
||||
|
||||
queue.addOperations(ops)
|
||||
case 'feed':
|
||||
ops.push({ op: 'feedLine', line: Number(feedLines.value) || 1 })
|
||||
break
|
||||
|
||||
case 'cut':
|
||||
ops.push({ op: 'cut', type: cutType.value })
|
||||
break
|
||||
|
||||
case 'pulse':
|
||||
ops.push({ op: 'pulse', drawer: pulseDrawer.value, time: pulseTime.value })
|
||||
break
|
||||
|
||||
case 'qr':
|
||||
if (qrData.value) {
|
||||
ops.push({
|
||||
op: 'qrcode',
|
||||
data: qrData.value,
|
||||
model: qrModel.value,
|
||||
level: qrLevel.value,
|
||||
size: qrSize.value
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'barcode':
|
||||
if (barcodeData.value) {
|
||||
ops.push({
|
||||
op: 'barcode',
|
||||
data: barcodeData.value,
|
||||
type: barcodeType.value,
|
||||
hri: barcodeHri.value,
|
||||
width: barcodeWidth.value,
|
||||
height: barcodeHeight.value
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (ops.length > 0) {
|
||||
queue.addOperations(ops)
|
||||
}
|
||||
}
|
||||
|
||||
// Validación para habilitar/deshabilitar botón
|
||||
const canAdd = computed(() => {
|
||||
switch (activeCommand.value) {
|
||||
case 'text':
|
||||
return text.value.trim().length > 0
|
||||
case 'feed':
|
||||
return feedLines.value != null && feedLines.value > 0
|
||||
case 'cut':
|
||||
case 'pulse':
|
||||
return true
|
||||
case 'qr':
|
||||
return qrData.value.trim().length > 0
|
||||
case 'barcode':
|
||||
return barcodeData.value.trim().length > 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -115,71 +270,165 @@ function queueText() {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Textarea principal -->
|
||||
<UFormField label="Texto a imprimir" class="mb-4">
|
||||
<UTextarea
|
||||
v-model="text"
|
||||
:rows="4"
|
||||
autoresize
|
||||
placeholder="Escribe el texto a imprimir..."
|
||||
/>
|
||||
</UFormField>
|
||||
<!-- Selector de tipo de comando -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<UButton
|
||||
v-for="cmd in commandTypes"
|
||||
:key="cmd.value"
|
||||
:variant="activeCommand === cmd.value ? 'solid' : 'outline'"
|
||||
:color="activeCommand === cmd.value ? 'primary' : 'neutral'"
|
||||
size="sm"
|
||||
:icon="cmd.icon"
|
||||
@click="activeCommand = cmd.value"
|
||||
>
|
||||
{{ cmd.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Controles de formato con Accordion -->
|
||||
<UAccordion :items="formatSections" type="multiple">
|
||||
<template #alignment>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 p-3">
|
||||
<UFormField label="Alineación">
|
||||
<USelect v-model="align" :items="alignOptions" placeholder="Seleccionar" />
|
||||
</UFormField>
|
||||
<UFormField label="Fuente">
|
||||
<USelect v-model="font" :items="fontOptions" placeholder="Seleccionar" />
|
||||
</UFormField>
|
||||
<UFormField label="Color">
|
||||
<USelect v-model="color" :items="colorOptions" placeholder="Seleccionar" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Formulario de TEXTO -->
|
||||
<div v-if="activeCommand === 'text'" class="space-y-4">
|
||||
<UFormField label="Texto a imprimir">
|
||||
<UTextarea
|
||||
v-model="text"
|
||||
:rows="4"
|
||||
autoresize
|
||||
placeholder="Escribe el texto a imprimir..."
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<template #size>
|
||||
<div class="grid grid-cols-2 gap-3 p-3">
|
||||
<UFormField label="Ancho (1-8)">
|
||||
<UInput v-model.number="width" type="number" :min="1" :max="8" placeholder="1" />
|
||||
</UFormField>
|
||||
<UFormField label="Alto (1-8)">
|
||||
<UInput v-model.number="height" type="number" :min="1" :max="8" placeholder="1" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
<UAccordion :items="formatSections" type="multiple">
|
||||
<template #alignment>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 p-3">
|
||||
<UFormField label="Alineación">
|
||||
<USelect v-model="align" :items="alignOptions" />
|
||||
</UFormField>
|
||||
<UFormField label="Fuente">
|
||||
<USelect v-model="font" :items="fontOptions" />
|
||||
</UFormField>
|
||||
<UFormField label="Color">
|
||||
<USelect v-model="color" :items="colorOptions" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #style>
|
||||
<div class="flex flex-wrap gap-4 p-3">
|
||||
<UCheckbox v-model="bold" label="Negrita" />
|
||||
<UCheckbox v-model="underline" label="Subrayado" />
|
||||
<UCheckbox v-model="reverse" label="Invertido" />
|
||||
<UCheckbox v-model="smooth" label="Suavizado" />
|
||||
</div>
|
||||
</template>
|
||||
<template #size>
|
||||
<div class="grid grid-cols-2 gap-3 p-3">
|
||||
<UFormField label="Ancho (1-8)">
|
||||
<UInput v-model.number="textWidth" type="number" :min="1" :max="8" placeholder="1" />
|
||||
</UFormField>
|
||||
<UFormField label="Alto (1-8)">
|
||||
<UInput v-model.number="textHeight" type="number" :min="1" :max="8" placeholder="1" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="grid grid-cols-2 gap-3 p-3">
|
||||
<UFormField label="Líneas de feed">
|
||||
<UInput v-model.number="feedLines" type="number" :min="0" :max="255" placeholder="0" />
|
||||
</UFormField>
|
||||
<UFormField label="Corte de papel">
|
||||
<USelect v-model="cut" :items="cutOptions" placeholder="Seleccionar" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
</UAccordion>
|
||||
<template #style>
|
||||
<div class="flex flex-wrap gap-4 p-3">
|
||||
<UCheckbox v-model="bold" label="Negrita" />
|
||||
<UCheckbox v-model="underline" label="Subrayado" />
|
||||
<UCheckbox v-model="reverse" label="Invertido" />
|
||||
<UCheckbox v-model="smooth" label="Suavizado" />
|
||||
</div>
|
||||
</template>
|
||||
</UAccordion>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de FEED -->
|
||||
<div v-else-if="activeCommand === 'feed'" class="space-y-4">
|
||||
<UFormField label="Líneas de avance">
|
||||
<UInput
|
||||
v-model.number="feedLines"
|
||||
type="number"
|
||||
:min="1"
|
||||
:max="255"
|
||||
placeholder="2"
|
||||
/>
|
||||
</UFormField>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Avanza el papel la cantidad de líneas especificadas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de CORTAR -->
|
||||
<div v-else-if="activeCommand === 'cut'" class="space-y-4">
|
||||
<UFormField label="Tipo de corte">
|
||||
<USelect v-model="cutType" :items="cutOptions" />
|
||||
</UFormField>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Corta el papel según el tipo seleccionado.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de PULSE -->
|
||||
<div v-else-if="activeCommand === 'pulse'" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="Drawer">
|
||||
<USelect v-model="pulseDrawer" :items="drawerOptions" />
|
||||
</UFormField>
|
||||
<UFormField label="Duración">
|
||||
<USelect v-model="pulseTime" :items="pulseTimeOptions" />
|
||||
</UFormField>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Envía un pulso al cajón de dinero para abrirlo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de QR -->
|
||||
<div v-else-if="activeCommand === 'qr'" class="space-y-4">
|
||||
<UFormField label="Datos del QR">
|
||||
<UTextarea
|
||||
v-model="qrData"
|
||||
:rows="2"
|
||||
placeholder="https://ejemplo.com o texto"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<UFormField label="Modelo">
|
||||
<USelect v-model="qrModel" :items="qrModelOptions" />
|
||||
</UFormField>
|
||||
<UFormField label="Corrección">
|
||||
<USelect v-model="qrLevel" :items="qrLevelOptions" />
|
||||
</UFormField>
|
||||
<UFormField label="Tamaño">
|
||||
<UInput v-model.number="qrSize" type="number" :min="1" :max="16" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de BARCODE -->
|
||||
<div v-else-if="activeCommand === 'barcode'" class="space-y-4">
|
||||
<UFormField label="Datos del código">
|
||||
<UInput
|
||||
v-model="barcodeData"
|
||||
placeholder="490123456789"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<UFormField label="Tipo">
|
||||
<USelect v-model="barcodeType" :items="barcodeTypeOptions" />
|
||||
</UFormField>
|
||||
<UFormField label="HRI (texto)">
|
||||
<USelect v-model="barcodeHri" :items="barcodeHriOptions" />
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<UFormField label="Ancho (2-6)">
|
||||
<UInput v-model.number="barcodeWidth" type="number" :min="2" :max="6" />
|
||||
</UFormField>
|
||||
<UFormField label="Alto (1-255)">
|
||||
<UInput v-model.number="barcodeHeight" type="number" :min="1" :max="255" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex gap-2">
|
||||
<UButton color="primary" @click="queueText">
|
||||
<UButton color="primary" :disabled="!canAdd" @click="addToQueue">
|
||||
Agregar a cola
|
||||
</UButton>
|
||||
<UButton variant="ghost" @click="resetForm">
|
||||
Limpiar formulario
|
||||
Limpiar
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user