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:
190
app/components/preview/PaperSimulator.vue
Normal file
190
app/components/preview/PaperSimulator.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import type { PreviewLine, TextSegment, FontType } from '~/composables/usePreview'
|
||||
import { CHAR_LIMITS } from '~/composables/usePreview'
|
||||
|
||||
const props = defineProps<{
|
||||
lines: PreviewLine[]
|
||||
font: FontType
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'edit-variable': [variableName: string, currentValue: string]
|
||||
}>()
|
||||
|
||||
// Ancho del papel según la fuente
|
||||
const paperWidth = computed(() => {
|
||||
return props.font === 'font_b' ? '40ch' : '33ch'
|
||||
})
|
||||
|
||||
// Obtener clases CSS para una línea
|
||||
function getLineClasses(line: PreviewLine): string[] {
|
||||
const classes: string[] = []
|
||||
|
||||
if (line.bold) classes.push('font-bold')
|
||||
if (line.underline) classes.push('underline')
|
||||
if (line.exceedsLimit) classes.push('line-exceeds')
|
||||
if (line.doubleWidth) classes.push('text-double-width')
|
||||
if (line.doubleHeight) classes.push('text-double-height')
|
||||
|
||||
// Alineación
|
||||
if (line.align === 'center') classes.push('text-center')
|
||||
else if (line.align === 'right') classes.push('text-right')
|
||||
else classes.push('text-left')
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
// Manejar click en variable
|
||||
function handleVariableClick(segment: TextSegment) {
|
||||
if (segment.type === 'variable' && segment.variableName) {
|
||||
emit('edit-variable', segment.variableName, segment.content)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="paper-simulator"
|
||||
:style="{ width: paperWidth }"
|
||||
>
|
||||
<template v-for="(line, index) in lines" :key="index">
|
||||
<!-- Línea vacía (feed) -->
|
||||
<div v-if="line.isFeed" class="line-feed"> </div>
|
||||
|
||||
<!-- Línea con contenido -->
|
||||
<div
|
||||
v-else
|
||||
class="line"
|
||||
:class="getLineClasses(line)"
|
||||
>
|
||||
<template v-for="(segment, segIndex) in line.segments" :key="segIndex">
|
||||
<!-- Segmento de variable (clickeable) -->
|
||||
<span
|
||||
v-if="segment.type === 'variable'"
|
||||
class="variable"
|
||||
@click="handleVariableClick(segment)"
|
||||
:title="`Variable: ${segment.variableName} (click para editar)`"
|
||||
>{{ segment.content }}</span>
|
||||
|
||||
<!-- Segmento de texto normal -->
|
||||
<span v-else>{{ segment.content }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Indicador de papel vacío -->
|
||||
<div v-if="lines.length === 0" class="empty-paper">
|
||||
<span class="text-gray-400">Vista previa vacía</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Contenedor del papel - simula papel térmico */
|
||||
.paper-simulator {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
background: linear-gradient(to bottom, #fefefe 0%, #f8f8f8 100%);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.1),
|
||||
inset 0 0 20px rgba(0, 0, 0, 0.02);
|
||||
/* Simular textura de papel */
|
||||
background-image:
|
||||
linear-gradient(to bottom, #fefefe 0%, #f8f8f8 100%),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 1px,
|
||||
rgba(0, 0, 0, 0.01) 1px,
|
||||
rgba(0, 0, 0, 0.01) 2px
|
||||
);
|
||||
}
|
||||
|
||||
/* Cada línea del ticket */
|
||||
.line {
|
||||
white-space: pre; /* Preservar espacios */
|
||||
min-height: 1.4em;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Línea vacía (feed) */
|
||||
.line-feed {
|
||||
height: 1.4em;
|
||||
}
|
||||
|
||||
/* Doble ancho - escalar horizontalmente */
|
||||
.text-double-width {
|
||||
transform: scaleX(2);
|
||||
transform-origin: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Doble alto - escalar verticalmente */
|
||||
.text-double-height {
|
||||
transform: scaleY(1.5);
|
||||
transform-origin: top;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* Línea que excede el límite */
|
||||
.line-exceeds {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.line-exceeds::after {
|
||||
content: '⚠';
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Variable editable - fondo azul suave */
|
||||
.variable {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.variable:hover {
|
||||
background: rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Papel vacío */
|
||||
.empty-paper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Scrollbar estilizado */
|
||||
.paper-simulator::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.paper-simulator::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.paper-simulator::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.paper-simulator::-webkit-scrollbar-thumb:hover {
|
||||
background: #aaa;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user