From 413ec6d27eeeceec4f9bea3649b49e61f818527f Mon Sep 17 00:00:00 2001 From: josedario87 Date: Wed, 26 Nov 2025 17:29:10 -0600 Subject: [PATCH] feat: Agregar visualizador de preview para templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuevo composable usePreview.ts para procesar operaciones en líneas de preview - Nuevo componente PaperSimulator.vue que simula el papel térmico - Nuevo modal PreviewModal.vue para vista previa con edición inline de variables - Botón "Vista previa" agregado a TemplateCard.vue - Integración del modal en TemplateList.vue --- app/components/preview/PaperSimulator.vue | 190 ++++++++++++++++ app/components/preview/PreviewModal.vue | 228 +++++++++++++++++++ app/components/templates/TemplateCard.vue | 27 ++- app/components/templates/TemplateList.vue | 47 ++++ app/composables/usePreview.ts | 266 ++++++++++++++++++++++ 5 files changed, 750 insertions(+), 8 deletions(-) create mode 100644 app/components/preview/PaperSimulator.vue create mode 100644 app/components/preview/PreviewModal.vue create mode 100644 app/composables/usePreview.ts diff --git a/app/components/preview/PaperSimulator.vue b/app/components/preview/PaperSimulator.vue new file mode 100644 index 0000000..5da3829 --- /dev/null +++ b/app/components/preview/PaperSimulator.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/app/components/preview/PreviewModal.vue b/app/components/preview/PreviewModal.vue new file mode 100644 index 0000000..2cbda98 --- /dev/null +++ b/app/components/preview/PreviewModal.vue @@ -0,0 +1,228 @@ + + + diff --git a/app/components/templates/TemplateCard.vue b/app/components/templates/TemplateCard.vue index 5aa3ad8..35fd21f 100644 --- a/app/components/templates/TemplateCard.vue +++ b/app/components/templates/TemplateCard.vue @@ -7,6 +7,7 @@ const props = defineProps<{ const emit = defineEmits<{ load: [id: string] + preview: [id: string] duplicate: [id: string] delete: [id: string] }>() @@ -70,14 +71,24 @@ function formatDate(date: string | number) { diff --git a/app/components/templates/TemplateList.vue b/app/components/templates/TemplateList.vue index 4e583e3..5c8af79 100644 --- a/app/components/templates/TemplateList.vue +++ b/app/components/templates/TemplateList.vue @@ -1,5 +1,6 @@ diff --git a/app/composables/usePreview.ts b/app/composables/usePreview.ts new file mode 100644 index 0000000..e038ec8 --- /dev/null +++ b/app/composables/usePreview.ts @@ -0,0 +1,266 @@ +import type { Operation } from './usePrintQueue' + +// Configuración de caracteres por línea según fuente y ancho +export const CHAR_LIMITS = { + font_a: { normal: 33, doubleWidth: 16 }, + font_b: { normal: 40, doubleWidth: 20 } +} as const + +export type FontType = 'font_a' | 'font_b' +export type AlignType = 'left' | 'center' | 'right' + +// Segmento de texto: puede ser texto normal o una variable +export interface TextSegment { + type: 'text' | 'variable' + content: string + variableName?: string // Solo para type='variable' +} + +// Una línea en el preview +export interface PreviewLine { + segments: TextSegment[] + fullText: string // Texto completo para calcular longitud + align: AlignType + bold: boolean + underline: boolean + doubleWidth: boolean + doubleHeight: boolean + font: FontType + charLimit: number + exceedsLimit: boolean + isFeed: boolean // true si es una línea vacía (feed) +} + +// Estado del preview +export interface PreviewState { + lines: PreviewLine[] + font: FontType + warnings: string[] + totalLines: number +} + +// Regex para detectar variables: {{nombre}} o {{nombre:label:default}} +const VARIABLE_REGEX = /\{\{(\w+)(?::([^:}]+))?(?::([^}]+))?\}\}/g + +// Parsear texto para extraer segmentos (texto normal y variables) +export function parseTextSegments(text: string): TextSegment[] { + const segments: TextSegment[] = [] + let lastIndex = 0 + let match: RegExpExecArray | null + + // Reset regex + VARIABLE_REGEX.lastIndex = 0 + + while ((match = VARIABLE_REGEX.exec(text)) !== null) { + // Agregar texto antes de la variable + if (match.index > lastIndex) { + segments.push({ + type: 'text', + content: text.slice(lastIndex, match.index) + }) + } + + // Agregar la variable + const [fullMatch, name, _label, defaultValue] = match + segments.push({ + type: 'variable', + content: defaultValue || name, // Mostrar default o nombre + variableName: name + }) + + lastIndex = match.index + fullMatch.length + } + + // Agregar texto restante + if (lastIndex < text.length) { + segments.push({ + type: 'text', + content: text.slice(lastIndex) + }) + } + + // Si no hay segmentos, el texto completo es normal + if (segments.length === 0 && text) { + segments.push({ type: 'text', content: text }) + } + + return segments +} + +// Obtener el texto completo de los segmentos +export function getFullText(segments: TextSegment[]): string { + return segments.map(s => s.content).join('') +} + +// Calcular límite de caracteres +export function getCharLimit(font: FontType, doubleWidth: boolean): number { + const limits = CHAR_LIMITS[font] + return doubleWidth ? limits.doubleWidth : limits.normal +} + +// Procesar operaciones en líneas de preview +export function processOperations( + operations: Operation[], + variableValues: Record = {} +): PreviewState { + const lines: PreviewLine[] = [] + const warnings: string[] = [] + + // Estado actual (se modifica con cada operación de estilo) + let currentState = { + font: 'font_a' as FontType, + align: 'left' as AlignType, + bold: false, + underline: false, + doubleWidth: false, + doubleHeight: false + } + + for (const op of operations) { + switch (op.op) { + case 'text': { + let text = op.value || '' + + // Reemplazar variables con valores proporcionados + const segments = parseTextSegments(text) + const resolvedSegments = segments.map(seg => { + if (seg.type === 'variable' && seg.variableName && variableValues[seg.variableName]) { + return { + ...seg, + content: variableValues[seg.variableName] + } + } + return seg + }) + + const fullText = getFullText(resolvedSegments) + const charLimit = getCharLimit(currentState.font, currentState.doubleWidth) + const exceedsLimit = fullText.length > charLimit + + if (exceedsLimit) { + warnings.push(`Línea excede ${charLimit} chars: "${fullText.slice(0, 20)}..."`) + } + + lines.push({ + segments: resolvedSegments, + fullText, + ...currentState, + charLimit, + exceedsLimit, + isFeed: false + }) + break + } + + case 'textAlign': + currentState.align = (op.align as AlignType) || 'left' + break + + case 'textFont': + if (op.font === 'font_a' || op.font === 'font_b') { + currentState.font = op.font + } + break + + case 'textSize': + currentState.doubleWidth = (op.width || 1) >= 2 + currentState.doubleHeight = (op.height || 1) >= 2 + break + + case 'textDouble': + currentState.doubleWidth = op.dw === true + currentState.doubleHeight = op.dh === true + break + + case 'textStyle': + if (op.em !== undefined) currentState.bold = op.em + if (op.ul !== undefined) currentState.underline = op.ul + break + + case 'feed': + case 'feedLine': { + const lineCount = op.line || op.lines || 1 + for (let i = 0; i < lineCount; i++) { + lines.push({ + segments: [], + fullText: '', + ...currentState, + charLimit: getCharLimit(currentState.font, currentState.doubleWidth), + exceedsLimit: false, + isFeed: true + }) + } + break + } + + case 'cut': + // El corte no agrega líneas visuales, solo indica fin + break + + // Ignorar operaciones que no afectan el preview visual + case 'pulse': + case 'textRotate': + case 'textLineSpace': + case 'feedUnit': + break + } + } + + return { + lines, + font: currentState.font, + warnings, + totalLines: lines.length + } +} + +// Composable para el preview +export function usePreview() { + const previewState = ref(null) + const variableValues = ref>({}) + + // Procesar operaciones y generar preview + function generatePreview(operations: Operation[], initialValues: Record = {}) { + variableValues.value = { ...initialValues } + previewState.value = processOperations(operations, variableValues.value) + } + + // Actualizar valor de una variable + function updateVariable(name: string, value: string, operations: Operation[]) { + variableValues.value[name] = value + previewState.value = processOperations(operations, variableValues.value) + } + + // Obtener todas las variables únicas de las operaciones + function extractVariables(operations: Operation[]): Array<{ name: string; defaultValue: string }> { + const variables = new Map() + + for (const op of operations) { + if (op.op === 'text' && op.value) { + let match: RegExpExecArray | null + VARIABLE_REGEX.lastIndex = 0 + + while ((match = VARIABLE_REGEX.exec(op.value)) !== null) { + const [, name, , defaultValue] = match + if (!variables.has(name)) { + variables.set(name, defaultValue || '') + } + } + } + } + + return Array.from(variables.entries()).map(([name, defaultValue]) => ({ + name, + defaultValue + })) + } + + return { + previewState: readonly(previewState), + variableValues: readonly(variableValues), + generatePreview, + updateVariable, + extractVariables, + CHAR_LIMITS + } +}