- 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
267 lines
7.2 KiB
TypeScript
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
|
|
}
|
|
}
|