Files
dreport/frontend/src/components/editor/LayoutRenderer.vue
2026-03-29 19:17:09 +03:00

263 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, inject, watch, nextTick } from 'vue'
import type { ElementLayout, LayoutResult } from '../../core/layout-types'
const props = defineProps<{
layout: LayoutResult | null
scale: number
}>()
// WASM barcode üretme fonksiyonu (EditorCanvas'tan provide edilir)
const generateBarcode = inject<(format: string, value: string, width: number, height: number) => Promise<{ width: number; height: number; rgba: ArrayBuffer } | null>>('generateBarcode')
const pageElements = computed(() => {
if (!props.layout || props.layout.pages.length === 0) return []
return props.layout.pages[0].elements
})
function elStyle(el: ElementLayout): Record<string, string> {
const s = props.scale
return {
position: 'absolute',
left: `${el.x_mm * s}px`,
top: `${el.y_mm * s}px`,
width: `${el.width_mm * s}px`,
height: `${el.height_mm * s}px`,
}
}
function textStyle(el: ElementLayout): Record<string, string> {
const s = props.scale
const st = el.style
const result: Record<string, string> = {}
// fontSize pt cinsinden → mm'ye çevir (1pt = 0.3528mm), sonra scale ile px'e
if (st.fontSize) result.fontSize = `${st.fontSize * 0.3528 * s}px`
if (st.fontWeight) result.fontWeight = st.fontWeight
if (st.fontFamily) result.fontFamily = st.fontFamily
if (st.color) result.color = st.color
if (st.textAlign) result.textAlign = st.textAlign
result.lineHeight = '1.2'
result.overflow = 'hidden'
result.wordBreak = 'break-word'
return result
}
function containerStyle(el: ElementLayout): Record<string, string> {
const st = el.style
const result: Record<string, string> = {}
if (st.backgroundColor) result.backgroundColor = st.backgroundColor
if (st.borderColor && st.borderWidth) {
result.border = `${st.borderWidth * props.scale}px ${st.borderStyle ?? 'solid'} ${st.borderColor}`
}
if (st.borderRadius) result.borderRadius = `${st.borderRadius * props.scale}px`
return result
}
function lineStyle(el: ElementLayout): Record<string, string> {
const st = el.style
return {
borderTop: `${(st.strokeWidth ?? 0.5) * props.scale}px solid ${st.strokeColor ?? '#000'}`,
width: '100%',
height: '0',
}
}
// --- Barcode rendering (WASM ile) ---
async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string, value: string, includeText: boolean = false) {
if (!value || !generateBarcode) return
try {
// WASM'dan yüksek çözünürlüklü pixel verisi al
// QR her zaman kare
const isQr = format === 'qr'
const size = isQr ? 300 : 400
const height = isQr ? 300 : 150
const result = await generateBarcode(format, value, size, height, isQr ? false : includeText)
if (!result) return
// Canvas boyutlarını WASM çıktısına ayarla (crisp rendering)
canvas.width = result.width
canvas.height = result.height
const ctx = canvas.getContext('2d')
if (!ctx) return
const imageData = new ImageData(
new Uint8ClampedArray(result.rgba),
result.width,
result.height,
)
ctx.putImageData(imageData, 0, 0)
} catch (e) {
console.warn(`[dreport] WASM barcode render hatası (${format}):`, e)
renderBarcodeFallback(canvas, format)
}
}
function renderBarcodeFallback(canvas: HTMLCanvasElement, format: string) {
canvas.width = 200
canvas.height = 80
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.fillStyle = '#f3f4f6'
ctx.fillRect(0, 0, 200, 80)
ctx.fillStyle = '#ef4444'
ctx.font = '11px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(`[${format}] hata`, 100, 44)
}
/** Canvas mount olduğunda render et */
function onBarcodeCanvasMounted(el: HTMLCanvasElement | null) {
if (!el) return
const format = el.dataset.format
const value = el.dataset.value
const includeText = el.dataset.includeText === 'true'
if (format && value) {
renderBarcodeToCanvas(el, format, value, includeText)
}
}
// Layout değiştiğinde tüm barcode canvas'ları yeniden render et
watch(
() => props.layout,
async () => {
await nextTick()
await nextTick()
const canvases = document.querySelectorAll<HTMLCanvasElement>('canvas[data-barcode]')
canvases.forEach(canvas => {
const format = canvas.dataset.format
const value = canvas.dataset.value
const includeText = canvas.dataset.includeText === 'true'
if (format && value) {
renderBarcodeToCanvas(canvas, format, value, includeText)
}
})
},
{ deep: true }
)
</script>
<template>
<div class="layout-renderer" v-if="layout">
<template v-for="el in pageElements" :key="el.id">
<!-- Container -->
<div
v-if="el.element_type === 'container'"
class="layout-el layout-el--container"
:style="{ ...elStyle(el), ...containerStyle(el) }"
/>
<!-- Static text / Text / Page number -->
<div
v-else-if="el.element_type === 'static_text' || el.element_type === 'text' || el.element_type === 'page_number'"
class="layout-el layout-el--text"
:style="{ ...elStyle(el), ...textStyle(el) }"
>
{{ el.content?.type === 'text' ? el.content.value : '' }}
</div>
<!-- Line -->
<div
v-else-if="el.element_type === 'line'"
class="layout-el layout-el--line"
:style="elStyle(el)"
>
<div :style="lineStyle(el)" />
</div>
<!-- Image -->
<div
v-else-if="el.element_type === 'image'"
class="layout-el layout-el--image"
:style="elStyle(el)"
>
<img
v-if="el.content?.type === 'image' && el.content.src"
:src="el.content.src"
:style="{
width: '100%',
height: '100%',
objectFit: 'fill',
}"
/>
<div v-else class="layout-el__placeholder">Görsel</div>
</div>
<!-- Barcode -->
<div
v-else-if="el.element_type === 'barcode'"
class="layout-el layout-el--barcode"
:style="elStyle(el)"
>
<canvas
v-if="el.content?.type === 'barcode' && el.content.value"
:ref="(ref) => onBarcodeCanvasMounted(ref as HTMLCanvasElement)"
data-barcode
:data-format="el.content.format"
:data-value="el.content.value"
:data-include-text="el.style.barcodeIncludeText ?? (el.content.format === 'ean13' || el.content.format === 'ean8')"
:style="{ width: '100%', height: '100%', display: 'block' }"
/>
<div v-else class="layout-el__placeholder">
{{ el.content?.type === 'barcode' ? `[${el.content.format}]` : '[barcode]' }}
</div>
</div>
</template>
</div>
<div class="layout-renderer layout-renderer--empty" v-else>
<span>Hesaplanıyor...</span>
</div>
</template>
<style scoped>
.layout-renderer {
position: absolute;
inset: 0;
pointer-events: none;
user-select: none;
}
.layout-renderer--empty {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
}
.layout-el {
box-sizing: border-box;
}
.layout-el--text {
white-space: pre-wrap;
font-family: 'Noto Sans', sans-serif;
}
.layout-el--line {
display: flex;
align-items: center;
}
.layout-el__placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
color: #9ca3af;
font-size: 11px;
border: 1px dashed #d1d5db;
border-radius: 2px;
}
</style>