Files
printerCentral/app/composables/usePreview.ts
josedario87 413ec6d27e feat: Agregar visualizador de preview para templates
- 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
2025-11-26 17:29:10 -06:00

267 lines
7.2 KiB
TypeScript

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<string, string> = {}
): 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<PreviewState | null>(null)
const variableValues = ref<Record<string, string>>({})
// Procesar operaciones y generar preview
function generatePreview(operations: Operation[], initialValues: Record<string, string> = {}) {
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<string, string>()
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
}
}