mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
refactor & improvements
This commit is contained in:
@@ -147,12 +147,7 @@ function applyZoom(delta: number, clientX: number, clientY: number) {
|
||||
const mousePageMmX = (clientX - pageRect.left) / oldScale
|
||||
const mousePageMmY = (clientY - pageRect.top) / oldScale
|
||||
|
||||
// Flex centering kayması: sayfa genişliği değişince ortalama kayar
|
||||
// X ekseni: justify-content: center → kayma = (eskiBoyut - yeniBoyut) / 2
|
||||
const pageW = templateStore.template.page.width
|
||||
const centerShiftX = pageW * (oldScale - newScale) / 2
|
||||
// Y ekseni: align-items: flex-start → kayma yok
|
||||
const centerShiftY = 0
|
||||
|
||||
// Yeni pan: mouse'un gösterdiği mm noktası aynı ekran pozisyonunda kalmalı
|
||||
const newPanX = editorStore.panX + (mousePageMmX - pageW / 2) * (oldScale - newScale)
|
||||
|
||||
@@ -17,7 +17,7 @@ const isSelected = computed(() => editorStore.selectedElementId === props.elemen
|
||||
const isContainerEl = computed(() => isContainer(props.element))
|
||||
const isAbsolute = computed(() => props.element.position.type === 'absolute')
|
||||
|
||||
// --- CSS style: layout'u Typst ile eşleştir ---
|
||||
// --- CSS style: layout engine sonuçlarına göre ---
|
||||
const layoutStyle = computed(() => {
|
||||
const el = props.element
|
||||
const s = props.scale
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ElementLayout } from '../../core/layout-types'
|
||||
import type { TemplateElement, SizeValue, ContainerElement } from '../../core/types'
|
||||
import { isContainer, sz } from '../../core/types'
|
||||
import ElementToolbar from './ElementToolbar.vue'
|
||||
import { useSnapGuides } from '../../composables/useSnapGuides'
|
||||
|
||||
const props = defineProps<{
|
||||
scale: number
|
||||
@@ -14,6 +15,7 @@ const props = defineProps<{
|
||||
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { activeGuides, collectEdges, calculateSnap, calculateResizeSnap, clearGuides } = useSnapGuides()
|
||||
|
||||
// Tüm elemanları flat olarak topla (root hariç)
|
||||
const flatElements = computed(() => {
|
||||
@@ -382,6 +384,8 @@ function onAbsoluteDragStart(e: PointerEvent, el: TemplateElement) {
|
||||
elY: el.position.y,
|
||||
}
|
||||
|
||||
collectEdges(props.layoutMap, el.id, templateStore.template.page.width, templateStore.template.page.height)
|
||||
|
||||
window.addEventListener('pointermove', onAbsoluteDragMove)
|
||||
window.addEventListener('pointerup', onAbsoluteDragEnd)
|
||||
}
|
||||
@@ -400,8 +404,16 @@ function onAbsoluteDragMove(e: PointerEvent) {
|
||||
}
|
||||
|
||||
const pxToMm = 1 / props.scale
|
||||
const newX = Math.max(0, absoluteDragStart.value.elX + dx * pxToMm)
|
||||
const newY = Math.max(0, absoluteDragStart.value.elY + dy * pxToMm)
|
||||
const proposedX = Math.max(0, absoluteDragStart.value.elX + dx * pxToMm)
|
||||
const proposedY = Math.max(0, absoluteDragStart.value.elY + dy * pxToMm)
|
||||
|
||||
const layout = props.layoutMap[absoluteDragId.value]
|
||||
const elW = layout ? layout.width_mm : 0
|
||||
const elH = layout ? layout.height_mm : 0
|
||||
|
||||
const snap = calculateSnap(proposedX, proposedY, elW, elH)
|
||||
const newX = snap.snappedX_mm
|
||||
const newY = snap.snappedY_mm
|
||||
|
||||
templateStore.updateElementPosition(absoluteDragId.value, {
|
||||
type: 'absolute',
|
||||
@@ -417,6 +429,7 @@ function onAbsoluteDragEnd() {
|
||||
isDragging.value = false
|
||||
absoluteDragId.value = null
|
||||
editorStore.setDragging(false)
|
||||
clearGuides()
|
||||
setTimeout(() => { didDrag.value = false }, 50)
|
||||
}
|
||||
|
||||
@@ -455,6 +468,8 @@ function onResizeStart(e: PointerEvent, elId: string, handle: string) {
|
||||
resizeGhost.value = { x: l.x_mm * s, y: l.y_mm * s, width: l.width_mm * s, height: l.height_mm * s }
|
||||
resizeFinalMm.value = { width: l.width_mm, height: l.height_mm }
|
||||
|
||||
collectEdges(props.layoutMap, elId, templateStore.template.page.width, templateStore.template.page.height)
|
||||
|
||||
window.addEventListener('pointermove', onResizeMove)
|
||||
window.addEventListener('pointerup', onResizeEnd)
|
||||
}
|
||||
@@ -485,11 +500,25 @@ function onResizeMove(e: PointerEvent) {
|
||||
|
||||
const startWMm = resizeStart.value.width * pxToMm
|
||||
const startHMm = resizeStart.value.height * pxToMm
|
||||
const startXMm = resizeStart.value.x * pxToMm
|
||||
const startYMm = resizeStart.value.y * pxToMm
|
||||
let wMm = startWMm, hMm = startHMm
|
||||
if (handle.includes('e')) wMm = Math.max(5, startWMm + dx * pxToMm)
|
||||
if (handle.includes('w')) wMm = Math.max(5, startWMm - dx * pxToMm)
|
||||
if (handle.includes('s')) hMm = Math.max(3, startHMm + dy * pxToMm)
|
||||
if (handle.includes('n')) hMm = Math.max(3, startHMm - dy * pxToMm)
|
||||
if (handle.includes('e')) {
|
||||
const rightEdge = calculateResizeSnap('right', startXMm + startWMm + dx * pxToMm)
|
||||
wMm = Math.max(5, rightEdge - startXMm)
|
||||
}
|
||||
if (handle.includes('w')) {
|
||||
const leftEdge = calculateResizeSnap('left', startXMm + dx * pxToMm)
|
||||
wMm = Math.max(5, startXMm + startWMm - leftEdge)
|
||||
}
|
||||
if (handle.includes('s')) {
|
||||
const bottomEdge = calculateResizeSnap('bottom', startYMm + startHMm + dy * pxToMm)
|
||||
hMm = Math.max(3, bottomEdge - startYMm)
|
||||
}
|
||||
if (handle.includes('n')) {
|
||||
const topEdge = calculateResizeSnap('top', startYMm + dy * pxToMm)
|
||||
hMm = Math.max(3, startYMm + startHMm - topEdge)
|
||||
}
|
||||
|
||||
if (ar > 0) {
|
||||
hMm = wMm / ar
|
||||
@@ -519,6 +548,7 @@ function onResizeEnd() {
|
||||
isResizing.value = false
|
||||
resizeElementId.value = null
|
||||
resizeHandle.value = ''
|
||||
clearGuides()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -629,6 +659,23 @@ const isAnyDragActive = computed(() =>
|
||||
<!-- Drop indicator (ortak — hem eleman hem toolbox sürükleme) -->
|
||||
<div v-if="isAnyDragActive" :style="dropIndicatorStyle" />
|
||||
|
||||
<!-- Snap guides -->
|
||||
<div
|
||||
v-for="(guide, gi) in activeGuides"
|
||||
:key="'guide-' + gi"
|
||||
class="snap-guide"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
...(guide.type === 'vertical'
|
||||
? { left: `${guide.position_mm * scale}px`, top: '0', bottom: '0', width: '1px' }
|
||||
: { top: `${guide.position_mm * scale}px`, left: '0', right: '0', height: '1px' }),
|
||||
background: '#3b82f6',
|
||||
opacity: 0.7,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- Element toolbar — seçili elemanın üstünde -->
|
||||
<ElementToolbar
|
||||
v-if="!isDragging && !isResizing"
|
||||
|
||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
// 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 generateBarcode = inject<(format: string, value: string, width: number, height: number, includeText: boolean) => Promise<{ width: number; height: number; rgba: ArrayBuffer } | null>>('generateBarcode')
|
||||
|
||||
const pageElements = computed(() => {
|
||||
if (!props.layout || props.layout.pages.length === 0) return []
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
svg: string | null
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="typst-svg-layer" v-if="svg" v-html="svg" />
|
||||
<div class="typst-svg-layer typst-svg-layer--empty" v-else>
|
||||
<span>Derleniyor...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.typst-svg-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typst-svg-layer :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.typst-svg-layer--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
266
frontend/src/components/panels/SchemaTreeNode.vue
Normal file
266
frontend/src/components/panels/SchemaTreeNode.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { SchemaNode } from '../../core/schema-parser'
|
||||
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
|
||||
import type { TemplateElement, RepeatingTableElement, TableColumn } from '../../core/types'
|
||||
import { sz } from '../../core/types'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
node: SchemaNode
|
||||
depth?: number
|
||||
}>(), {
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
const expanded = ref(props.depth < 2)
|
||||
|
||||
let colIdCounter = 0
|
||||
|
||||
const isScalar = ['string', 'number', 'integer', 'boolean'].includes(props.node.type)
|
||||
const isArray = props.node.type === 'array'
|
||||
const isObject = props.node.type === 'object'
|
||||
const isDraggable = isScalar || isArray
|
||||
const hasChildren = isObject
|
||||
? props.node.children.length > 0
|
||||
: isArray
|
||||
? (props.node.itemProperties?.length ?? 0) > 0
|
||||
: false
|
||||
|
||||
const typeIcon: Record<string, string> = {
|
||||
string: 'Aa',
|
||||
number: '#',
|
||||
integer: '#',
|
||||
boolean: '\u2713',
|
||||
object: '{ }',
|
||||
array: '[ ]',
|
||||
}
|
||||
|
||||
const borderColor: Record<string, string> = {
|
||||
string: '#3b82f6',
|
||||
number: '#22c55e',
|
||||
integer: '#22c55e',
|
||||
boolean: '#f59e0b',
|
||||
object: '#94a3b8',
|
||||
array: '#8b5cf6',
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (hasChildren) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
}
|
||||
|
||||
function createBoundTextElement(node: SchemaNode): TemplateElement {
|
||||
return {
|
||||
id: `txt_${Date.now().toString(36)}`,
|
||||
type: 'text',
|
||||
position: { type: 'flow' },
|
||||
size: { width: sz.auto(), height: sz.auto() },
|
||||
style: { fontSize: 11, color: '#000000' },
|
||||
binding: { type: 'scalar', path: node.path },
|
||||
}
|
||||
}
|
||||
|
||||
function createBoundTableElement(node: SchemaNode): RepeatingTableElement {
|
||||
const itemFields = schemaStore.getArrayItemFields(node.path)
|
||||
const columns: TableColumn[] = itemFields.map(field => ({
|
||||
id: `col_${(++colIdCounter).toString(36)}`,
|
||||
field: field.key,
|
||||
title: field.title,
|
||||
width: sz.auto(),
|
||||
align: defaultAlignForSchema(field),
|
||||
format: schemaFormatToFormatType(field.format),
|
||||
}))
|
||||
return {
|
||||
id: `tbl_${Date.now().toString(36)}`,
|
||||
type: 'repeating_table',
|
||||
position: { type: 'flow' },
|
||||
size: { width: sz.fr(1), height: sz.auto() },
|
||||
dataSource: { type: 'array', path: node.path },
|
||||
columns,
|
||||
style: { headerBg: '#f0f0f0', headerColor: '#000000', fontSize: 10, headerFontSize: 10 },
|
||||
}
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent) {
|
||||
if (!isDraggable) return
|
||||
|
||||
let el: TemplateElement
|
||||
if (isScalar) {
|
||||
el = createBoundTextElement(props.node)
|
||||
} else {
|
||||
el = createBoundTableElement(props.node)
|
||||
}
|
||||
|
||||
editorStore.startDragNewElement(el)
|
||||
e.dataTransfer?.setData('text/plain', el.id)
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
editorStore.endDragNewElement()
|
||||
}
|
||||
|
||||
const displayChildren = isArray
|
||||
? (props.node.itemProperties ?? [])
|
||||
: props.node.children
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="schema-node">
|
||||
<div
|
||||
class="schema-node__row"
|
||||
:class="{
|
||||
'schema-node__row--draggable': isDraggable,
|
||||
'schema-node__row--object': isObject,
|
||||
}"
|
||||
:style="{
|
||||
paddingLeft: `${depth * 16 + 8}px`,
|
||||
borderLeftColor: borderColor[node.type] ?? '#94a3b8',
|
||||
}"
|
||||
:draggable="isDraggable"
|
||||
:title="node.path || node.key"
|
||||
@click="toggle"
|
||||
@dragstart="onDragStart"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<span v-if="hasChildren" class="schema-node__arrow" :class="{ 'schema-node__arrow--expanded': expanded }">
|
||||
▶
|
||||
</span>
|
||||
<span v-else class="schema-node__arrow-placeholder" />
|
||||
|
||||
<span class="schema-node__type-icon" :class="`schema-node__type-icon--${node.type}`">
|
||||
{{ typeIcon[node.type] ?? '?' }}
|
||||
</span>
|
||||
|
||||
<span class="schema-node__title">{{ node.title }}</span>
|
||||
|
||||
<span v-if="isScalar && node.path" class="schema-node__path">{{ node.path }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hasChildren && expanded" class="schema-node__children">
|
||||
<SchemaTreeNode
|
||||
v-for="child in displayChildren"
|
||||
:key="child.path"
|
||||
:node="child"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.schema-node__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 8px;
|
||||
border-left: 3px solid transparent;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
border-radius: 0 4px 4px 0;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.schema-node__row--draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.schema-node__row--draggable:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.schema-node__row:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.schema-node__row--draggable:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.schema-node__arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
color: #94a3b8;
|
||||
transition: transform 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.schema-node__arrow--expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.schema-node__arrow-placeholder {
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.schema-node__type-icon {
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.schema-node__type-icon--string {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.schema-node__type-icon--number,
|
||||
.schema-node__type-icon--integer {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.schema-node__type-icon--boolean {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.schema-node__type-icon--object {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.schema-node__type-icon--array {
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.schema-node__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schema-node__path {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 80px;
|
||||
}
|
||||
</style>
|
||||
51
frontend/src/components/panels/SchemaTreePanel.vue
Normal file
51
frontend/src/components/panels/SchemaTreePanel.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import SchemaTreeNode from './SchemaTreeNode.vue'
|
||||
|
||||
const schemaStore = useSchemaStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="schema-panel">
|
||||
<div class="schema-panel__title">Schema</div>
|
||||
<div v-if="schemaStore.schemaTree.children.length === 0" class="schema-panel__empty">
|
||||
Schema yuklu degil
|
||||
</div>
|
||||
<div v-else class="schema-panel__tree">
|
||||
<SchemaTreeNode
|
||||
v-for="child in schemaStore.schemaTree.children"
|
||||
:key="child.path"
|
||||
:node="child"
|
||||
:depth="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.schema-panel {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.schema-panel__title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.schema-panel__empty {
|
||||
padding: 20px 12px;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.schema-panel__tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
152
frontend/src/components/properties/BarcodeProperties.vue
Normal file
152
frontend/src/components/properties/BarcodeProperties.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import type { BarcodeElement, BarcodeFormat, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: BarcodeElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
const barcodeDefaults: Record<BarcodeFormat, string> = {
|
||||
qr: 'https://example.com',
|
||||
ean13: '5901234123457',
|
||||
ean8: '96385074',
|
||||
code128: 'DREPORT-001',
|
||||
code39: 'DREPORT',
|
||||
}
|
||||
|
||||
function eanCheckDigit(data: string): number {
|
||||
let sum = 0
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const d = parseInt(data[i])
|
||||
sum += d * (i % 2 === 0 ? 1 : 3)
|
||||
}
|
||||
return (10 - (sum % 10)) % 10
|
||||
}
|
||||
|
||||
function validateBarcode(format: BarcodeFormat, value: string): boolean {
|
||||
if (!value) return false
|
||||
switch (format) {
|
||||
case 'ean13':
|
||||
if (!/^\d{13}$/.test(value)) return false
|
||||
return eanCheckDigit(value.slice(0, 12)) === parseInt(value[12])
|
||||
case 'ean8':
|
||||
if (!/^\d{8}$/.test(value)) return false
|
||||
return eanCheckDigit(value.slice(0, 7)) === parseInt(value[7])
|
||||
case 'code39':
|
||||
return /^[A-Z0-9\-. $/+%]+$/i.test(value)
|
||||
case 'code128':
|
||||
return value.length > 0 && [...value].every(c => c.charCodeAt(0) < 128)
|
||||
case 'qr':
|
||||
return value.length > 0
|
||||
default:
|
||||
return value.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
const barcodeInputValue = ref('')
|
||||
const barcodeInputInvalid = ref(false)
|
||||
|
||||
watch(() => props.element.value ?? '', (val) => {
|
||||
barcodeInputValue.value = val
|
||||
barcodeInputInvalid.value = false
|
||||
}, { immediate: true })
|
||||
|
||||
function onBarcodeValueInput(e: Event) {
|
||||
const val = (e.target as HTMLInputElement).value
|
||||
barcodeInputValue.value = val
|
||||
|
||||
if (validateBarcode(props.element.format, val)) {
|
||||
barcodeInputInvalid.value = false
|
||||
update({ value: val } as any)
|
||||
} else {
|
||||
barcodeInputInvalid.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onBarcodeFormatChange(newFormat: BarcodeFormat) {
|
||||
const currentValue = props.element.value ?? ''
|
||||
if (validateBarcode(newFormat, currentValue)) {
|
||||
update({ format: newFormat } as any)
|
||||
} else {
|
||||
const defaultVal = barcodeDefaults[newFormat]
|
||||
barcodeInputValue.value = defaultVal
|
||||
barcodeInputInvalid.value = false
|
||||
update({ format: newFormat, value: defaultVal } as any)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Barkod Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Format</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.format"
|
||||
@change="(e) => onBarcodeFormatChange((e.target as HTMLSelectElement).value as BarcodeFormat)">
|
||||
<option value="qr">QR Kod</option>
|
||||
<option value="ean13">EAN-13</option>
|
||||
<option value="ean8">EAN-8</option>
|
||||
<option value="code128">Code 128</option>
|
||||
<option value="code39">Code 39</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Deger</label>
|
||||
<input class="prop-input" type="text"
|
||||
:class="{ 'prop-input--invalid': barcodeInputInvalid }"
|
||||
:value="barcodeInputValue"
|
||||
@input="onBarcodeValueInput" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Renk</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.color" class="prop-clear" @click="updateStyle('color', undefined)">x</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="element.format !== 'qr'" class="prop-row">
|
||||
<label class="prop-label">Metin Goster</label>
|
||||
<input type="checkbox"
|
||||
:checked="element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')"
|
||||
@change="(e) => updateStyle('includeText', (e.target as HTMLInputElement).checked)" />
|
||||
</div>
|
||||
<div v-if="schemaStore.scalarFields.length > 0" class="prop-row">
|
||||
<label class="prop-label">Veri Baglama</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.binding?.path ?? ''"
|
||||
@change="(e) => {
|
||||
const val = (e.target as HTMLSelectElement).value
|
||||
if (val) {
|
||||
update({ binding: { type: 'scalar', path: val } } as any)
|
||||
} else {
|
||||
update({ binding: undefined } as any)
|
||||
}
|
||||
}">
|
||||
<option value="">Yok (statik deger)</option>
|
||||
<option
|
||||
v-for="field in schemaStore.scalarFields"
|
||||
:key="field.path"
|
||||
:value="field.path"
|
||||
>{{ field.title }} ({{ field.path }})</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
115
frontend/src/components/properties/ContainerProperties.vue
Normal file
115
frontend/src/components/properties/ContainerProperties.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import PaddingBox from './PaddingBox.vue'
|
||||
import type { ContainerElement, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: ContainerElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Container Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Yon</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.direction"
|
||||
@change="(e) => update({ direction: (e.target as HTMLSelectElement).value } as any)">
|
||||
<option value="column">Dikey</option>
|
||||
<option value="row">Yatay</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Bosluk (mm)</label>
|
||||
<input class="prop-input" type="number" step="1" min="0"
|
||||
:value="element.gap"
|
||||
@input="(e) => update({ gap: parseFloat((e.target as HTMLInputElement).value) || 0 } as any)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">{{ element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama' }}</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.align"
|
||||
@change="(e) => update({ align: (e.target as HTMLSelectElement).value } as any)">
|
||||
<option value="start">{{ element.direction === 'column' ? 'Sol' : 'Ust' }}</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="end">{{ element.direction === 'column' ? 'Sag' : 'Alt' }}</option>
|
||||
<option value="stretch">Esnet</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">{{ element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim' }}</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.justify"
|
||||
@change="(e) => update({ justify: (e.target as HTMLSelectElement).value } as any)">
|
||||
<option value="start">{{ element.direction === 'column' ? 'Ust' : 'Sol' }}</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="end">{{ element.direction === 'column' ? 'Alt' : 'Sag' }}</option>
|
||||
<option value="space-between">Esit Aralik</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="prop-section__subtitle">Padding (mm)</div>
|
||||
<PaddingBox
|
||||
:top="element.padding.top"
|
||||
:right="element.padding.right"
|
||||
:bottom="element.padding.bottom"
|
||||
:left="element.padding.left"
|
||||
@update="(side, value) => update({ padding: { ...element.padding, [side]: value } } as any)"
|
||||
/>
|
||||
|
||||
<div class="prop-section__subtitle">Stil</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Arka plan</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.backgroundColor ?? '#ffffff'"
|
||||
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.backgroundColor" class="prop-clear" @click="updateStyle('backgroundColor', undefined)">x</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kenarlik (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.1" min="0"
|
||||
:value="element.style.borderWidth ?? 0"
|
||||
@input="(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kenarlik rengi</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.borderColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.borderColor" class="prop-clear" @click="updateStyle('borderColor', undefined)">x</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kenarlik stili</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.style.borderStyle ?? 'solid'"
|
||||
@change="(e) => updateStyle('borderStyle', (e.target as HTMLSelectElement).value)">
|
||||
<option value="solid">Duz</option>
|
||||
<option value="dashed">Kesikli</option>
|
||||
<option value="dotted">Noktali</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Radius (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.5" min="0"
|
||||
:value="element.style.borderRadius ?? 0"
|
||||
@input="(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
62
frontend/src/components/properties/ImageProperties.vue
Normal file
62
frontend/src/components/properties/ImageProperties.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { ImageElement, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: ImageElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function onImageFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
update({ src: reader.result as string } as Partial<TemplateElement>)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Gorsel</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kaynak</label>
|
||||
<label class="prop-file-btn">
|
||||
Dosya Sec
|
||||
<input type="file" accept="image/*" style="display: none" @change="onImageFileSelect" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="element.src" class="prop-row">
|
||||
<label class="prop-label">Onizleme</label>
|
||||
<img :src="element.src" class="prop-image-preview" />
|
||||
</div>
|
||||
<div v-if="element.src" class="prop-row">
|
||||
<label class="prop-label"></label>
|
||||
<button class="prop-clear" @click="update({ src: undefined } as any)">Gorseli kaldir</button>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Sigdirma</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.style.objectFit ?? 'contain'"
|
||||
@change="(e) => updateStyle('objectFit', (e.target as HTMLSelectElement).value)">
|
||||
<option value="contain">Sigdir</option>
|
||||
<option value="cover">Kap</option>
|
||||
<option value="stretch">Esnet</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
34
frontend/src/components/properties/LineProperties.vue
Normal file
34
frontend/src/components/properties/LineProperties.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { LineElement, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: LineElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, { style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Cizgi Stili</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kalinlik (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.1" min="0.1"
|
||||
:value="element.style.strokeWidth ?? 0.5"
|
||||
@input="(e) => updateStyle('strokeWidth', parseFloat((e.target as HTMLInputElement).value) || 0.5)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.strokeColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('strokeColor', (e.target as HTMLInputElement).value)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
59
frontend/src/components/properties/PageNumberProperties.vue
Normal file
59
frontend/src/components/properties/PageNumberProperties.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { PageNumberElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: PageNumberElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Sayfa Numarasi</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Format</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.format ?? '{current} / {total}'"
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)">
|
||||
<option value="{current} / {total}">1 / 5</option>
|
||||
<option value="{current}">1</option>
|
||||
<option value="Sayfa {current}">Sayfa 1</option>
|
||||
<option value="Sayfa {current} / {total}">Sayfa 1 / 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 10"
|
||||
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#666666'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'center'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
43
frontend/src/components/properties/PositioningProperties.vue
Normal file
43
frontend/src/components/properties/PositioningProperties.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import type { TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
|
||||
function togglePositioning() {
|
||||
if (props.element.position.type === 'flow') {
|
||||
templateStore.updateElementPosition(props.element.id, { type: 'absolute', x: 0, y: 0 })
|
||||
} else {
|
||||
templateStore.updateElementPosition(props.element.id, { type: 'flow' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Pozisyon</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Mod</label>
|
||||
<select class="prop-input prop-select" :value="element.position.type" @change="togglePositioning">
|
||||
<option value="flow">Flow</option>
|
||||
<option value="absolute">Absolute</option>
|
||||
</select>
|
||||
</div>
|
||||
<template v-if="element.position.type === 'absolute'">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">X (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.5"
|
||||
:value="element.position.x"
|
||||
@input="(e) => templateStore.updateElementPosition(element.id, { type: 'absolute', x: parseFloat((e.target as HTMLInputElement).value) || 0, y: (element.position as any).y ?? 0 })" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Y (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.5"
|
||||
:value="element.position.y"
|
||||
@input="(e) => templateStore.updateElementPosition(element.id, { type: 'absolute', x: (element.position as any).x ?? 0, y: parseFloat((e.target as HTMLInputElement).value) || 0 })" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
242
frontend/src/components/properties/RepeatingTableProperties.vue
Normal file
242
frontend/src/components/properties/RepeatingTableProperties.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import { sz } from '../../core/types'
|
||||
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
|
||||
import type { RepeatingTableElement, TableColumn, FormatType, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: RepeatingTableElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
let colIdCounter = Date.now()
|
||||
function nextColId() {
|
||||
return `col_${(++colIdCounter).toString(36)}`
|
||||
}
|
||||
|
||||
function updateTableDataSource(path: string) {
|
||||
const itemFields = schemaStore.getArrayItemFields(path)
|
||||
if (itemFields.length > 0) {
|
||||
const columns: TableColumn[] = itemFields.map(field => ({
|
||||
id: nextColId(),
|
||||
field: field.key,
|
||||
title: field.title,
|
||||
width: sz.auto(),
|
||||
align: defaultAlignForSchema(field),
|
||||
format: schemaFormatToFormatType(field.format),
|
||||
}))
|
||||
update({
|
||||
dataSource: { type: 'array', path },
|
||||
columns,
|
||||
} as Partial<TemplateElement>)
|
||||
} else {
|
||||
update({ dataSource: { type: 'array', path } } as Partial<TemplateElement>)
|
||||
}
|
||||
}
|
||||
|
||||
function updateTableStyle(key: string, value: unknown) {
|
||||
const newStyle = { ...props.element.style, [key]: value }
|
||||
if (value === undefined || value === '') delete (newStyle as Record<string, unknown>)[key]
|
||||
update({ style: newStyle } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function updateColumn(colId: string, updates: Partial<TableColumn>) {
|
||||
const columns = props.element.columns.map(c => c.id === colId ? { ...c, ...updates } : c)
|
||||
update({ columns } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function addColumn() {
|
||||
const newCol: TableColumn = {
|
||||
id: nextColId(),
|
||||
field: 'alan',
|
||||
title: 'Yeni Sutun',
|
||||
width: sz.auto(),
|
||||
align: 'left',
|
||||
}
|
||||
update({ columns: [...props.element.columns, newCol] } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function removeColumn(colId: string) {
|
||||
update({ columns: props.element.columns.filter(c => c.id !== colId) } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function moveColumn(colId: string, direction: -1 | 1) {
|
||||
const cols = [...props.element.columns]
|
||||
const idx = cols.findIndex(c => c.id === colId)
|
||||
const newIdx = idx + direction
|
||||
if (newIdx < 0 || newIdx >= cols.length) return
|
||||
;[cols[idx], cols[newIdx]] = [cols[newIdx], cols[idx]]
|
||||
update({ columns: cols } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
const tableItemFields = computed(() => {
|
||||
return schemaStore.getArrayItemFields(props.element.dataSource.path)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Data source -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Veri Kaynagi</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kaynak</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.dataSource.path"
|
||||
@change="(e) => updateTableDataSource((e.target as HTMLSelectElement).value)">
|
||||
<option value="" disabled>Secin...</option>
|
||||
<option
|
||||
v-for="arr in schemaStore.arrayFields"
|
||||
:key="arr.path"
|
||||
:value="arr.path"
|
||||
>{{ arr.title }} ({{ arr.path }})</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columns -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">
|
||||
Sutunlar
|
||||
<button class="prop-add-btn" @click="addColumn">+</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="col in element.columns"
|
||||
:key="col.id"
|
||||
class="prop-column-card"
|
||||
>
|
||||
<div class="prop-column-header">
|
||||
<span class="prop-column-title">{{ col.title || col.field }}</span>
|
||||
<div class="prop-column-actions">
|
||||
<button class="prop-icon-btn" @click="moveColumn(col.id, -1)" title="Yukari">↑</button>
|
||||
<button class="prop-icon-btn" @click="moveColumn(col.id, 1)" title="Asagi">↓</button>
|
||||
<button class="prop-icon-btn prop-icon-btn--danger" @click="removeColumn(col.id)" title="Sil">x</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Baslik</label>
|
||||
<input class="prop-input" type="text" :value="col.title"
|
||||
@change="(e) => updateColumn(col.id, { title: (e.target as HTMLInputElement).value })" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Alan</label>
|
||||
<select v-if="tableItemFields.length > 0" class="prop-input prop-select" :value="col.field"
|
||||
@change="(e) => {
|
||||
const field = (e.target as HTMLSelectElement).value
|
||||
const node = tableItemFields.find(f => f.key === field)
|
||||
if (node) {
|
||||
updateColumn(col.id, {
|
||||
field,
|
||||
title: node.title,
|
||||
align: defaultAlignForSchema(node),
|
||||
format: schemaFormatToFormatType(node.format),
|
||||
})
|
||||
} else {
|
||||
updateColumn(col.id, { field })
|
||||
}
|
||||
}">
|
||||
<option v-for="f in tableItemFields" :key="f.key" :value="f.key">{{ f.title }} ({{ f.key }})</option>
|
||||
</select>
|
||||
<input v-else class="prop-input" type="text" :value="col.field"
|
||||
@change="(e) => updateColumn(col.id, { field: (e.target as HTMLInputElement).value })" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select" :value="col.align"
|
||||
@change="(e) => updateColumn(col.id, { align: (e.target as HTMLSelectElement).value as 'left'|'center'|'right' })">
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Format</label>
|
||||
<select class="prop-input prop-select" :value="col.format ?? ''"
|
||||
@change="(e) => updateColumn(col.id, { format: ((e.target as HTMLSelectElement).value || undefined) as FormatType | undefined })">
|
||||
<option value="">Yok</option>
|
||||
<option value="currency">Para birimi</option>
|
||||
<option value="number">Sayi</option>
|
||||
<option value="date">Tarih</option>
|
||||
<option value="percentage">Yuzde</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Genislik</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="col.width.type"
|
||||
@change="(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateColumn(col.id, { width: { type: 'auto' } })
|
||||
else if (t === 'fr') updateColumn(col.id, { width: { type: 'fr', value: 1 } })
|
||||
else updateColumn(col.id, { width: { type: 'fixed', value: 30 } })
|
||||
}">
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="col.width.type === 'fixed'" class="prop-row">
|
||||
<label class="prop-label">mm</label>
|
||||
<input class="prop-input" type="number" step="1" min="5"
|
||||
:value="(col.width as any).value"
|
||||
@change="(e) => updateColumn(col.id, { width: { type: 'fixed', value: parseFloat((e.target as HTMLInputElement).value) || 30 } })" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table style -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Tablo Stili</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Yazi boyutu</label>
|
||||
<input class="prop-input" type="number" step="1" min="6"
|
||||
:value="element.style.fontSize ?? 10"
|
||||
@input="(e) => updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Header bg</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.headerBg ?? '#f0f0f0'"
|
||||
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Header renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.headerColor ?? '#000000'"
|
||||
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Zebra tek</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.zebraOdd ?? '#fafafa'"
|
||||
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.zebraOdd" class="prop-clear" @click="updateTableStyle('zebraOdd', undefined)">x</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kenarlik rengi</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="element.style.borderColor ?? '#cccccc'"
|
||||
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.borderColor" class="prop-clear" @click="updateTableStyle('borderColor', undefined)">x</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kenarlik (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.1" min="0"
|
||||
:value="element.style.borderWidth ?? 0.5"
|
||||
@input="(e) => updateTableStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
67
frontend/src/components/properties/SizeProperties.vue
Normal file
67
frontend/src/components/properties/SizeProperties.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import type { TemplateElement, SizeValue } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
|
||||
function updateSize(axis: 'width' | 'height', sv: SizeValue) {
|
||||
templateStore.updateElementSize(props.element.id, { [axis]: sv })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Boyut</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Genislik</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.size.width.type"
|
||||
@change="(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateSize('width', { type: 'auto' })
|
||||
else if (t === 'fr') updateSize('width', { type: 'fr', value: 1 })
|
||||
else updateSize('width', { type: 'fixed', value: 50 })
|
||||
}">
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="element.size.width.type === 'fixed'" class="prop-row">
|
||||
<label class="prop-label">mm</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
:value="(element.size.width as any).value"
|
||||
@input="(e) => updateSize('width', { type: 'fixed', value: parseFloat((e.target as HTMLInputElement).value) || 10 })" />
|
||||
</div>
|
||||
<div v-if="element.size.width.type === 'fr'" class="prop-row">
|
||||
<label class="prop-label">fr</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
:value="(element.size.width as any).value"
|
||||
@input="(e) => updateSize('width', { type: 'fr', value: parseFloat((e.target as HTMLInputElement).value) || 1 })" />
|
||||
</div>
|
||||
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Yukseklik</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="element.size.height.type"
|
||||
@change="(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateSize('height', { type: 'auto' })
|
||||
else if (t === 'fr') updateSize('height', { type: 'fr', value: 1 })
|
||||
else updateSize('height', { type: 'fixed', value: 20 })
|
||||
}">
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="element.size.height.type === 'fixed'" class="prop-row">
|
||||
<label class="prop-label">mm</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
:value="(element.size.height as any).value"
|
||||
@input="(e) => updateSize('height', { type: 'fixed', value: parseFloat((e.target as HTMLInputElement).value) || 10 })" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
65
frontend/src/components/properties/TextProperties.vue
Normal file
65
frontend/src/components/properties/TextProperties.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { StaticTextElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Metin Stili</div>
|
||||
|
||||
<div v-if="element.type === 'static_text'" class="prop-row">
|
||||
<label class="prop-label">Metin</label>
|
||||
<input class="prop-input" type="text"
|
||||
:value="(element as StaticTextElement).content"
|
||||
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)" />
|
||||
</div>
|
||||
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 11"
|
||||
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kalinlik</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
|
||||
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Kalin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user