feat: Variables programáticas en templates
Permite definir variables en templates con sintaxis {{nombre🏷️default}}
- Auto-detección de variables al guardar templates
- Drawer para completar valores al cargar template con variables
- Badge mostrando cantidad de variables en tarjeta de template
- Resolución de variables antes de cargar en cola
This commit is contained in:
@@ -51,10 +51,13 @@ function formatDate(date: string | number) {
|
|||||||
>
|
>
|
||||||
{{ template.description }}
|
{{ template.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2 mt-2">
|
<div class="flex items-center gap-2 mt-2 flex-wrap">
|
||||||
<UBadge variant="subtle" size="xs">
|
<UBadge variant="subtle" size="xs">
|
||||||
{{ template.operations.length }} comandos
|
{{ template.operations.length }} comandos
|
||||||
</UBadge>
|
</UBadge>
|
||||||
|
<UBadge v-if="template.variables?.length" variant="subtle" size="xs" color="primary">
|
||||||
|
{{ template.variables.length }} {{ template.variables.length === 1 ? 'variable' : 'variables' }}
|
||||||
|
</UBadge>
|
||||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
{{ formatDate(template.updatedAt) }}
|
{{ formatDate(template.updatedAt) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,21 +1,48 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { PrintTemplate } from '~/composables/useTemplates'
|
||||||
|
import { resolveVariables } from '~/composables/useTemplates'
|
||||||
|
|
||||||
const templates = useTemplates()
|
const templates = useTemplates()
|
||||||
const queue = usePrintQueue()
|
const queue = usePrintQueue()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
// Estado para el drawer de variables
|
||||||
|
const variablesDrawerOpen = ref(false)
|
||||||
|
const selectedTemplate = ref<PrintTemplate | null>(null)
|
||||||
|
|
||||||
// Cargar templates al montar
|
// Cargar templates al montar
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
templates.fetchTemplates()
|
templates.fetchTemplates()
|
||||||
})
|
})
|
||||||
|
|
||||||
function loadTemplate(id: string) {
|
function loadTemplate(id: string) {
|
||||||
const ops = templates.loadTemplate(id)
|
const template = templates.templates.value.find(t => t.id === id)
|
||||||
if (ops) {
|
if (!template) return
|
||||||
queue.loadFromTemplate(ops)
|
|
||||||
toast.add({ title: 'Template cargado en la cola', color: 'success' })
|
// Si tiene variables, abrir el drawer
|
||||||
|
if (template.variables && template.variables.length > 0) {
|
||||||
|
selectedTemplate.value = template
|
||||||
|
variablesDrawerOpen.value = true
|
||||||
|
} else {
|
||||||
|
// Sin variables, cargar directo
|
||||||
|
const ops = templates.loadTemplate(id)
|
||||||
|
if (ops) {
|
||||||
|
queue.loadFromTemplate(ops)
|
||||||
|
toast.add({ title: 'Template cargado en la cola', color: 'success' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleLoadWithVariables(values: Record<string, string>) {
|
||||||
|
if (!selectedTemplate.value) return
|
||||||
|
|
||||||
|
const ops = resolveVariables(selectedTemplate.value.operations, values)
|
||||||
|
queue.loadFromTemplate(ops)
|
||||||
|
toast.add({ title: 'Template cargado en la cola', color: 'success' })
|
||||||
|
variablesDrawerOpen.value = false
|
||||||
|
selectedTemplate.value = null
|
||||||
|
}
|
||||||
|
|
||||||
async function duplicateTemplate(id: string) {
|
async function duplicateTemplate(id: string) {
|
||||||
const result = await templates.duplicateTemplate(id)
|
const result = await templates.duplicateTemplate(id)
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -72,5 +99,13 @@ async function deleteTemplate(id: string) {
|
|||||||
@delete="deleteTemplate"
|
@delete="deleteTemplate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Drawer para completar variables -->
|
||||||
|
<TemplatesVariablesDrawer
|
||||||
|
:template="selectedTemplate"
|
||||||
|
:open="variablesDrawerOpen"
|
||||||
|
@update:open="variablesDrawerOpen = $event"
|
||||||
|
@load="handleLoadWithVariables"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
88
app/components/templates/VariablesDrawer.vue
Normal file
88
app/components/templates/VariablesDrawer.vue
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrintTemplate } from '~/composables/useTemplates'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
template: PrintTemplate | null
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
load: [values: Record<string, string>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const values = ref<Record<string, string>>({})
|
||||||
|
|
||||||
|
// Inicializar con valores por defecto cuando cambia el template
|
||||||
|
watch(() => props.template, (t) => {
|
||||||
|
if (t) {
|
||||||
|
values.value = {}
|
||||||
|
for (const v of t.variables || []) {
|
||||||
|
values.value[v.name] = v.defaultValue || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
function handleLoad() {
|
||||||
|
emit('load', { ...values.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrawerUpdate(value: boolean) {
|
||||||
|
emit('update:open', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit('update:open', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que todos los campos requeridos estén completos
|
||||||
|
const canLoad = computed(() => {
|
||||||
|
if (!props.template?.variables) return false
|
||||||
|
return props.template.variables.every(v => values.value[v.name]?.trim())
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDrawer
|
||||||
|
:open="open"
|
||||||
|
direction="bottom"
|
||||||
|
title="Completar variables"
|
||||||
|
description="Ingresa los valores para las variables del template"
|
||||||
|
@update:open="handleDrawerUpdate"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4 p-4">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Template: <strong>{{ template?.name }}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UFormField
|
||||||
|
v-for="v in template?.variables || []"
|
||||||
|
:key="v.name"
|
||||||
|
:label="v.label || v.name"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="values[v.name]"
|
||||||
|
:placeholder="v.defaultValue || `Ingrese ${v.label || v.name}`"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex gap-2 justify-end p-4">
|
||||||
|
<UButton variant="ghost" @click="handleClose">
|
||||||
|
Cancelar
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
:disabled="!canLoad"
|
||||||
|
@click="handleLoad"
|
||||||
|
>
|
||||||
|
Cargar en cola
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDrawer>
|
||||||
|
</template>
|
||||||
@@ -1,14 +1,40 @@
|
|||||||
import type { Operation } from './usePrintQueue'
|
import type { Operation } from './usePrintQueue'
|
||||||
|
|
||||||
|
export interface TemplateVariable {
|
||||||
|
name: string
|
||||||
|
label?: string
|
||||||
|
defaultValue?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface PrintTemplate {
|
export interface PrintTemplate {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
operations: Operation[]
|
operations: Operation[]
|
||||||
|
variables: TemplateVariable[]
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regex para detectar variables en templates
|
||||||
|
const VARIABLE_REGEX = /\{\{(\w+)(?::([^:}]+))?(?::([^}]+))?\}\}/g
|
||||||
|
|
||||||
|
// Resolver variables en operaciones
|
||||||
|
export function resolveVariables(
|
||||||
|
operations: Operation[],
|
||||||
|
values: Record<string, string>
|
||||||
|
): Operation[] {
|
||||||
|
return JSON.parse(JSON.stringify(operations)).map((op: Operation) => {
|
||||||
|
for (const [key, val] of Object.entries(op)) {
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
// Reemplazar {{nombre:label:default}} con el valor proporcionado
|
||||||
|
op[key] = val.replace(VARIABLE_REGEX, (_, name) => values[name] ?? '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return op
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useTemplates() {
|
export function useTemplates() {
|
||||||
const templates = useState<PrintTemplate[]>('templates', () => [])
|
const templates = useState<PrintTemplate[]>('templates', () => [])
|
||||||
const loading = useState('templatesLoading', () => false)
|
const loading = useState('templatesLoading', () => false)
|
||||||
@@ -119,6 +145,7 @@ export function useTemplates() {
|
|||||||
updateTemplate,
|
updateTemplate,
|
||||||
deleteTemplate,
|
deleteTemplate,
|
||||||
loadTemplate,
|
loadTemplate,
|
||||||
duplicateTemplate
|
duplicateTemplate,
|
||||||
|
resolveVariables
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeTab === 'templates'">
|
<template v-else-if="activeTab === 'templates'">
|
||||||
<TemplatesTemplateList />
|
<ClientOnly>
|
||||||
|
<TemplatesTemplateList />
|
||||||
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeTab === 'printers'">
|
<template v-else-if="activeTab === 'printers'">
|
||||||
@@ -79,9 +81,11 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- En desktop: mostrar templates debajo del constructor -->
|
<!-- En desktop: mostrar templates debajo del constructor -->
|
||||||
<template v-if="isDesktop">
|
<ClientOnly>
|
||||||
<TemplatesTemplateList />
|
<template v-if="isDesktop">
|
||||||
</template>
|
<TemplatesTemplateList />
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ import { existsSync } from 'fs'
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
export interface Operation {
|
export interface Operation {
|
||||||
id: string
|
op: string
|
||||||
type: 'text' | 'feed' | 'cut' | 'pulse' | 'image' | 'barcode' | 'qrcode'
|
[key: string]: any
|
||||||
params: Record<string, unknown>
|
}
|
||||||
|
|
||||||
|
export interface TemplateVariable {
|
||||||
|
name: string
|
||||||
|
label?: string
|
||||||
|
defaultValue?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrintTemplate {
|
export interface PrintTemplate {
|
||||||
@@ -14,10 +19,42 @@ export interface PrintTemplate {
|
|||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
operations: Operation[]
|
operations: Operation[]
|
||||||
|
variables: TemplateVariable[]
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regex para detectar {{nombre}}, {{nombre:label}}, {{nombre:label:default}}
|
||||||
|
const VARIABLE_REGEX = /\{\{(\w+)(?::([^:}]+))?(?::([^}]+))?\}\}/g
|
||||||
|
|
||||||
|
// Extraer variables de las operaciones
|
||||||
|
export function extractVariables(operations: Operation[]): TemplateVariable[] {
|
||||||
|
const variablesMap = new Map<string, TemplateVariable>()
|
||||||
|
|
||||||
|
for (const op of operations) {
|
||||||
|
// Buscar en campos que pueden contener variables
|
||||||
|
const searchFields = ['value', 'data']
|
||||||
|
for (const field of searchFields) {
|
||||||
|
if (typeof op[field] === 'string') {
|
||||||
|
const matches = op[field].matchAll(VARIABLE_REGEX)
|
||||||
|
for (const match of matches) {
|
||||||
|
const [, name, label, defaultValue] = match
|
||||||
|
// Solo guardar la primera definición de cada variable
|
||||||
|
if (!variablesMap.has(name)) {
|
||||||
|
variablesMap.set(name, {
|
||||||
|
name,
|
||||||
|
label: label || undefined,
|
||||||
|
defaultValue: defaultValue || undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(variablesMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
export interface TemplatesStore {
|
export interface TemplatesStore {
|
||||||
templates: PrintTemplate[]
|
templates: PrintTemplate[]
|
||||||
}
|
}
|
||||||
@@ -84,6 +121,7 @@ export async function createTemplate(data: {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
operations: data.operations,
|
operations: data.operations,
|
||||||
|
variables: extractVariables(data.operations),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now
|
||||||
}
|
}
|
||||||
@@ -103,9 +141,15 @@ export async function updateTemplate(id: string, data: Partial<{
|
|||||||
|
|
||||||
if (index === -1) return null
|
if (index === -1) return null
|
||||||
|
|
||||||
|
// Si se actualizan las operaciones, recalcular las variables
|
||||||
|
const variables = data.operations
|
||||||
|
? extractVariables(data.operations)
|
||||||
|
: store.templates[index].variables
|
||||||
|
|
||||||
store.templates[index] = {
|
store.templates[index] = {
|
||||||
...store.templates[index],
|
...store.templates[index],
|
||||||
...data,
|
...data,
|
||||||
|
variables,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user