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
This commit is contained in:
266
app/composables/usePreview.ts
Normal file
266
app/composables/usePreview.ts
Normal file
@@ -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<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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user