- Agregar imports de Tailwind CSS v4 y Nuxt UI en main.css - Renombrar QueueActions.vue -> Actions.vue y QueueItem.vue -> Item.vue para evitar conflictos de nombres de componentes - Crear composable useMediaQuery para manejo de responsive - Corregir referencias a componentes en index.vue y PrintQueue.vue - Actualizar imports de servidor a rutas relativas - Instalar @iconify-json/heroicons y @iconify-json/lucide - Actualizar Jimp a sintaxis v1.x
184 lines
5.2 KiB
Vue
184 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import type { Operation } from '~/composables/usePrintQueue'
|
|
|
|
const props = defineProps<{
|
|
operation: Operation
|
|
index: number
|
|
isFirst: boolean
|
|
isLast: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
update: [op: Operation]
|
|
remove: []
|
|
'move-up': []
|
|
'move-down': []
|
|
}>()
|
|
|
|
const isEditing = ref(false)
|
|
const editableFields = reactive<Record<string, string>>({})
|
|
|
|
// Color según tipo de operación
|
|
const opColor = computed(() => {
|
|
const op = props.operation.op
|
|
if (op.startsWith('text')) return 'primary'
|
|
if (op.startsWith('feed')) return 'info'
|
|
if (op === 'cut') return 'warning'
|
|
if (op === 'pulse') return 'success'
|
|
if (op === 'barcode' || op === 'qrcode') return 'secondary'
|
|
return 'neutral'
|
|
})
|
|
|
|
// Icono según tipo de operación
|
|
const opIcon = computed(() => {
|
|
const op = props.operation.op
|
|
if (op === 'text') return 'i-heroicons-document-text'
|
|
if (op.startsWith('text')) return 'i-heroicons-adjustments-horizontal'
|
|
if (op.startsWith('feed')) return 'i-heroicons-arrow-down'
|
|
if (op === 'cut') return 'i-heroicons-scissors'
|
|
if (op === 'pulse') return 'i-heroicons-bolt'
|
|
if (op === 'barcode') return 'i-heroicons-bars-3'
|
|
if (op === 'qrcode') return 'i-heroicons-qr-code'
|
|
return 'i-heroicons-cog-6-tooth'
|
|
})
|
|
|
|
// Inicializar campos editables
|
|
function initEditableFields() {
|
|
Object.keys(editableFields).forEach(k => delete editableFields[k])
|
|
for (const [k, v] of Object.entries(props.operation)) {
|
|
if (k === 'op') continue
|
|
editableFields[k] = typeof v === 'object' ? JSON.stringify(v) : String(v)
|
|
}
|
|
}
|
|
|
|
watch(() => props.operation, initEditableFields, { immediate: true, deep: true })
|
|
|
|
function applyChanges() {
|
|
const updated: Operation = { op: props.operation.op }
|
|
for (const [k, v] of Object.entries(editableFields)) {
|
|
try {
|
|
updated[k] = JSON.parse(v)
|
|
} catch {
|
|
const n = Number(v)
|
|
updated[k] = isNaN(n) ? v : n
|
|
}
|
|
}
|
|
emit('update', updated)
|
|
isEditing.value = false
|
|
}
|
|
|
|
function cancelEdit() {
|
|
initEditableFields()
|
|
isEditing.value = false
|
|
}
|
|
|
|
// Resumen legible del comando
|
|
const summary = computed(() => {
|
|
const op = props.operation
|
|
switch (op.op) {
|
|
case 'text':
|
|
return op.value?.substring(0, 30) + (op.value?.length > 30 ? '...' : '')
|
|
case 'textAlign':
|
|
return `Alinear: ${op.align}`
|
|
case 'textFont':
|
|
return `Fuente: ${op.font}`
|
|
case 'textSize':
|
|
return `Tamaño: ${op.width || 1}x${op.height || 1}`
|
|
case 'textStyle':
|
|
const styles = []
|
|
if (op.em) styles.push('negrita')
|
|
if (op.ul) styles.push('subrayado')
|
|
if (op.reverse) styles.push('invertido')
|
|
return styles.length ? `Estilo: ${styles.join(', ')}` : 'Estilo: normal'
|
|
case 'feedLine':
|
|
return `Feed: ${op.line} líneas`
|
|
case 'cut':
|
|
return `Cortar: ${op.type}`
|
|
case 'pulse':
|
|
return `Pulse: ${op.drawer}`
|
|
case 'qrcode':
|
|
return `QR: ${op.data?.substring(0, 20)}...`
|
|
case 'barcode':
|
|
return `Barcode: ${op.data}`
|
|
default:
|
|
return op.op
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UCard variant="soft" class="group">
|
|
<div class="flex items-start gap-3">
|
|
<!-- Indicador de tipo -->
|
|
<div class="flex flex-col items-center gap-1">
|
|
<UBadge :color="opColor" variant="subtle" size="xs">
|
|
{{ index + 1 }}
|
|
</UBadge>
|
|
<UIcon :name="opIcon" class="w-4 h-4 text-gray-400" />
|
|
</div>
|
|
|
|
<!-- Contenido -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="font-medium text-sm text-gray-900 dark:text-white">
|
|
{{ operation.op }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Vista normal -->
|
|
<p v-if="!isEditing" class="text-sm text-gray-500 dark:text-gray-400 truncate">
|
|
{{ summary }}
|
|
</p>
|
|
|
|
<!-- Vista edición -->
|
|
<div v-else class="space-y-2 mt-2">
|
|
<div v-for="(value, key) in editableFields" :key="key" class="flex items-center gap-2">
|
|
<span class="text-xs text-gray-500 w-16 shrink-0">{{ key }}:</span>
|
|
<UInput
|
|
v-model="editableFields[key]"
|
|
size="xs"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
<div class="flex gap-2 mt-2">
|
|
<UButton size="xs" @click="applyChanges">Aplicar</UButton>
|
|
<UButton size="xs" variant="ghost" @click="cancelEdit">Cancelar</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Acciones -->
|
|
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<UButton
|
|
v-if="!isEditing"
|
|
icon="i-heroicons-pencil"
|
|
variant="ghost"
|
|
size="xs"
|
|
@click="isEditing = true"
|
|
/>
|
|
<UButton
|
|
icon="i-heroicons-chevron-up"
|
|
variant="ghost"
|
|
size="xs"
|
|
:disabled="isFirst"
|
|
@click="$emit('move-up')"
|
|
/>
|
|
<UButton
|
|
icon="i-heroicons-chevron-down"
|
|
variant="ghost"
|
|
size="xs"
|
|
:disabled="isLast"
|
|
@click="$emit('move-down')"
|
|
/>
|
|
<UButton
|
|
icon="i-heroicons-trash"
|
|
variant="ghost"
|
|
size="xs"
|
|
color="error"
|
|
@click="$emit('remove')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</template>
|