mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
visual testing
This commit is contained in:
@@ -604,11 +604,7 @@ async function downloadPdf() {
|
||||
const blob = await editorRef.value?.exportPdf()
|
||||
if (!blob) return
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${template.value.name || 'belge'}.pdf`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
window.open(url, '_blank')
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'PDF olusturulamadi')
|
||||
} finally {
|
||||
@@ -668,7 +664,7 @@ function resetTemplate() {
|
||||
<!-- Output -->
|
||||
<button class="header-btn" :disabled="pdfLoading" @click="downloadPdf">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="1" width="10" height="14" rx="1.5" /><path d="M6 5h4M6 8h4M6 11h2" /></svg>
|
||||
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Indir' }}
|
||||
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Onizle' }}
|
||||
</button>
|
||||
</header>
|
||||
<DreportEditor
|
||||
|
||||
@@ -3,12 +3,15 @@ import { computed } from 'vue'
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { isContainer } from '../../core/types'
|
||||
import type { ContainerElement, TextStyle, RepeatingTableElement, TableStyle } from '../../core/types'
|
||||
import type { ElementLayout } from '../../core/layout-types'
|
||||
import type { ContainerElement, TextStyle, RepeatingTableElement, TableStyle, ChartElement, ChartType } from '../../core/types'
|
||||
import type { LayoutMapEntry } from '../../core/layout-types'
|
||||
|
||||
const PAGE_GAP_PX = 24
|
||||
|
||||
const props = defineProps<{
|
||||
scale: number
|
||||
layoutMap: Record<string, ElementLayout>
|
||||
layoutMap: Record<string, LayoutMapEntry>
|
||||
pageHeightPx?: number
|
||||
}>()
|
||||
|
||||
const templateStore = useTemplateStore()
|
||||
@@ -36,6 +39,15 @@ const isTable = computed(() => selected.value?.type === 'repeating_table')
|
||||
const tableEl = computed(() => isTable.value ? selected.value as RepeatingTableElement : null)
|
||||
const tableStyle = computed(() => tableEl.value?.style as TableStyle | undefined)
|
||||
|
||||
const isChart = computed(() => selected.value?.type === 'chart')
|
||||
const chartEl = computed(() => isChart.value ? selected.value as ChartElement : null)
|
||||
|
||||
function pageYOffset(pageIndex: number): number {
|
||||
if (pageIndex <= 0) return 0
|
||||
const pageH = props.pageHeightPx ?? (templateStore.template.page.height * props.scale)
|
||||
return pageIndex * (pageH + PAGE_GAP_PX)
|
||||
}
|
||||
|
||||
const toolbarStyle = computed(() => {
|
||||
const el = selected.value
|
||||
if (!el) return { display: 'none' }
|
||||
@@ -43,10 +55,11 @@ const toolbarStyle = computed(() => {
|
||||
if (!l) return { display: 'none' }
|
||||
|
||||
const s = props.scale
|
||||
const pYOff = pageYOffset(l.pageIndex)
|
||||
return {
|
||||
position: 'absolute' as const,
|
||||
left: `${l.x_mm * s}px`,
|
||||
top: `${l.y_mm * s - 30}px`,
|
||||
top: `${l.y_mm * s - 30 + pYOff}px`,
|
||||
zIndex: 1100,
|
||||
}
|
||||
})
|
||||
@@ -76,6 +89,13 @@ function updateTableStyle(key: string, value: unknown) {
|
||||
if (!selected.value) return
|
||||
update({ style: { ...selected.value.style, [key]: value } })
|
||||
}
|
||||
|
||||
// Chart
|
||||
function setChartType(t: ChartType) { update({ chartType: t }) }
|
||||
function updateChartStyle(key: string, value: unknown) {
|
||||
if (!selected.value) return
|
||||
update({ style: { ...selected.value.style, [key]: value } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -300,6 +320,73 @@ function updateTableStyle(key: string, value: unknown) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ===== Chart ===== -->
|
||||
<template v-if="isChart && chartEl">
|
||||
<!-- Chart type -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chartEl.chartType === 'bar' }" data-tip="Cubuk" @click="setChartType('bar')">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<rect x="2" y="6" width="3" height="6" rx="0.5" fill="currentColor"/>
|
||||
<rect x="5.5" y="3" width="3" height="9" rx="0.5" fill="currentColor"/>
|
||||
<rect x="9" y="5" width="3" height="7" rx="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chartEl.chartType === 'line' }" data-tip="Cizgi" @click="setChartType('line')">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<polyline points="2,10 5,5 8,7 12,3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<circle cx="2" cy="10" r="1.2" fill="currentColor"/><circle cx="5" cy="5" r="1.2" fill="currentColor"/><circle cx="8" cy="7" r="1.2" fill="currentColor"/><circle cx="12" cy="3" r="1.2" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chartEl.chartType === 'pie' }" data-tip="Pasta" @click="setChartType('pie')">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M7 2a5 5 0 1 1-3.54 1.46" stroke="currentColor" stroke-width="1.3" fill="none"/>
|
||||
<path d="M7 7V2A5 5 0 0 0 3.46 3.46Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Show labels -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chartEl.labels?.show !== false }" data-tip="Etiketler" @click="update({ labels: { ...chartEl.labels, show: chartEl.labels?.show === false ? true : false } })">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<rect x="2" y="8" width="3" height="4" rx="0.5" fill="currentColor" opacity="0.4"/>
|
||||
<rect x="5.5" y="5" width="3" height="7" rx="0.5" fill="currentColor" opacity="0.4"/>
|
||||
<rect x="9" y="6" width="3" height="6" rx="0.5" fill="currentColor" opacity="0.4"/>
|
||||
<text x="3.5" y="7" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">3</text>
|
||||
<text x="7" y="4" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">7</text>
|
||||
<text x="10.5" y="5" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">5</text>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Show grid -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chartEl.axis?.showGrid !== false }" data-tip="Izgara" @click="update({ axis: { ...chartEl.axis, showGrid: chartEl.axis?.showGrid === false ? true : false } })">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<line x1="2" y1="3" x2="12" y2="3" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5"/>
|
||||
<line x1="2" y1="7" x2="12" y2="7" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5"/>
|
||||
<line x1="2" y1="11" x2="12" y2="11" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Background color -->
|
||||
<div class="et__group">
|
||||
<label class="et__color-wrap" data-tip="Arka Plan">
|
||||
<input type="color" class="et__color" :value="chartEl.style.backgroundColor ?? '#ffffff'" @input="(e) => updateChartStyle('backgroundColor', (e.target as HTMLInputElement).value)" />
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<rect x="2" y="2" width="10" height="10" rx="1.5" :fill="chartEl.style.backgroundColor ?? '#ffffff'" stroke="#94a3b8" stroke-width="0.8"/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ===== Line ===== -->
|
||||
<template v-if="isLine">
|
||||
<!-- Stroke width -->
|
||||
|
||||
@@ -713,6 +713,7 @@ const isAnyDragActive = computed(() =>
|
||||
v-if="!isDragging && !isResizing"
|
||||
:scale="scale"
|
||||
:layout-map="layoutMap"
|
||||
:page-height-px="pageHeightPx"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
121
frontend/src/render-test/RenderTestPage.vue
Normal file
121
frontend/src/render-test/RenderTestPage.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, onMounted } from 'vue'
|
||||
import LayoutRenderer from '../components/editor/LayoutRenderer.vue'
|
||||
import { useLayoutEngine } from '../composables/useLayoutEngine'
|
||||
import type { Template } from '../core/types'
|
||||
|
||||
// Fixture data is injected by Playwright via window.__DREPORT_FIXTURE__
|
||||
declare global {
|
||||
interface Window {
|
||||
__DREPORT_FIXTURE__?: { template: string; data: string }
|
||||
}
|
||||
}
|
||||
|
||||
// DPI matching: 150 DPI = 150/25.4 px/mm (must match pdftoppm -r 150)
|
||||
const DPI = 150
|
||||
const SCALE = DPI / 25.4
|
||||
|
||||
const ready = ref(false)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
|
||||
const template = ref<Template>({
|
||||
id: 'empty',
|
||||
name: 'empty',
|
||||
page: { width: 210, height: 297 },
|
||||
fonts: ['Noto Sans'],
|
||||
root: {
|
||||
id: 'root', type: 'container',
|
||||
position: { type: 'flow' },
|
||||
size: { width: { type: 'auto' }, height: { type: 'auto' } },
|
||||
direction: 'column', gap: 0,
|
||||
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
align: 'stretch', justify: 'start',
|
||||
style: {}, children: [],
|
||||
},
|
||||
})
|
||||
const data = ref<Record<string, unknown>>({})
|
||||
|
||||
const { layout, generateBarcode, error } = useLayoutEngine(template, data)
|
||||
|
||||
// Provide barcode generator for LayoutRenderer
|
||||
provide('generateBarcode', generateBarcode)
|
||||
|
||||
// Watch for layout computation to complete
|
||||
const checkReady = setInterval(() => {
|
||||
if (layout.value && layout.value.pages.length > 0) {
|
||||
ready.value = true
|
||||
clearInterval(checkReady)
|
||||
}
|
||||
if (error.value) {
|
||||
errorMsg.value = error.value
|
||||
clearInterval(checkReady)
|
||||
}
|
||||
}, 50)
|
||||
|
||||
// Timeout after 20 seconds
|
||||
setTimeout(() => {
|
||||
clearInterval(checkReady)
|
||||
if (!ready.value) {
|
||||
errorMsg.value = 'Layout computation timed out'
|
||||
}
|
||||
}, 20000)
|
||||
|
||||
onMounted(() => {
|
||||
const fixture = window.__DREPORT_FIXTURE__
|
||||
if (!fixture) {
|
||||
errorMsg.value = 'No fixture data found. Set window.__DREPORT_FIXTURE__ = { template, data }'
|
||||
return
|
||||
}
|
||||
try {
|
||||
template.value = JSON.parse(fixture.template)
|
||||
data.value = JSON.parse(fixture.data)
|
||||
} catch (e) {
|
||||
errorMsg.value = `Failed to parse fixture data: ${e}`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="render-test-root"
|
||||
:data-render-ready="ready || undefined"
|
||||
:data-render-error="errorMsg || undefined"
|
||||
>
|
||||
<LayoutRenderer
|
||||
v-if="layout"
|
||||
:layout="layout"
|
||||
:scale="SCALE"
|
||||
/>
|
||||
<div v-else-if="errorMsg" class="error">{{ errorMsg }}</div>
|
||||
<div v-else class="loading">Computing layout...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.render-test-root {
|
||||
/* No padding/margin — pixel-exact rendering */
|
||||
background: white;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
padding: 20px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Override layout-page styles for test — no shadow, no margin */
|
||||
.layout-page {
|
||||
box-shadow: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.layout-page + .layout-page {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
4
frontend/src/render-test/main.ts
Normal file
4
frontend/src/render-test/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import RenderTestPage from './RenderTestPage.vue'
|
||||
|
||||
createApp(RenderTestPage).mount('#app')
|
||||
Reference in New Issue
Block a user