mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
fixes
This commit is contained in:
@@ -230,6 +230,26 @@ pub struct ChartAxis {
|
||||
pub y_label: Option<String>,
|
||||
pub show_grid: Option<bool>,
|
||||
pub grid_color: Option<String>,
|
||||
/// Show vertical grid lines at each category (line charts). Defaults to true.
|
||||
pub show_vertical_grid: Option<bool>,
|
||||
pub vertical_grid_color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub reference_lines: Vec<ChartReferenceLine>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChartReferenceLine {
|
||||
/// Category index (0-based) where the vertical line is drawn
|
||||
pub category_index: usize,
|
||||
#[serde(default)]
|
||||
pub color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub width: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub dash: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
|
||||
@@ -32,6 +32,7 @@ import RichTextProperties from '../properties/RichTextProperties.vue'
|
||||
import ContainerProperties from '../properties/ContainerProperties.vue'
|
||||
import RepeatingTableProperties from '../properties/RepeatingTableProperties.vue'
|
||||
import ChartProperties from '../properties/ChartProperties.vue'
|
||||
import PropCondition from '../properties/shared/PropCondition.vue'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const templateStore = useTemplateStore()
|
||||
@@ -233,6 +234,13 @@ function deleteSelected() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Condition -->
|
||||
<PropCondition
|
||||
v-if="selectedElement.id !== 'root'"
|
||||
:condition="selectedElement.condition"
|
||||
@update:condition="(v) => templateStore.updateElement(selectedElement!.id, { condition: v } as any)"
|
||||
/>
|
||||
|
||||
<!-- Delete -->
|
||||
<div v-if="selectedElement.id !== 'root'" class="prop-section">
|
||||
<button class="prop-delete-btn" @click="deleteElement">Sil</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEditorStore } from '../../stores/editor'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import type {
|
||||
TemplateElement,
|
||||
TextElement,
|
||||
RepeatingTableElement,
|
||||
TableColumn,
|
||||
ImageElement,
|
||||
@@ -46,6 +47,18 @@ const tools: ToolItem[] = [
|
||||
content: 'Yeni metin',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Veri Metni',
|
||||
icon: 'D',
|
||||
create: (): TextElement => ({
|
||||
id: nextId('dtxt'),
|
||||
type: 'text',
|
||||
position: { type: 'flow' },
|
||||
size: { width: sz.auto(), height: sz.auto() },
|
||||
style: { fontSize: 11, color: '#000000' },
|
||||
binding: { type: 'scalar', path: '' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Zengin Metin',
|
||||
icon: 'R',
|
||||
|
||||
@@ -42,10 +42,12 @@ const formatOptions = [
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 11"
|
||||
:font-weight="style().fontWeight ?? 'normal'"
|
||||
:font-family="style().fontFamily"
|
||||
:color="style().color ?? '#000000'"
|
||||
:align="style().align ?? 'left'"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
|
||||
@@ -247,6 +247,19 @@ function removeColor(index: number) {
|
||||
:model-value="element.axis?.gridColor ?? '#E5E7EB'"
|
||||
@update:model-value="(v) => updateNested('axis', 'gridColor', v, {})"
|
||||
/>
|
||||
<template v-if="element.chartType === 'line'">
|
||||
<PropCheckbox
|
||||
label="Dikey Izgara"
|
||||
:model-value="element.axis?.showVerticalGrid ?? true"
|
||||
@update:model-value="(v) => updateNested('axis', 'showVerticalGrid', v, {})"
|
||||
/>
|
||||
<PropColorInput
|
||||
v-if="element.axis?.showVerticalGrid !== false"
|
||||
label="Dikey Izgara Renk"
|
||||
:model-value="element.axis?.verticalGridColor ?? '#E5E7EB'"
|
||||
@update:model-value="(v) => updateNested('axis', 'verticalGridColor', v, {})"
|
||||
/>
|
||||
</template>
|
||||
</PropSection>
|
||||
|
||||
<!-- Stil -->
|
||||
@@ -292,6 +305,12 @@ function removeColor(index: number) {
|
||||
:min="0.1"
|
||||
@update:model-value="(v) => updateStyle('lineWidth', v)"
|
||||
/>
|
||||
<PropSelect
|
||||
label="Egri Tipi"
|
||||
:model-value="element.style.curveType ?? 'linear'"
|
||||
:options="[{ value: 'linear', label: 'Duz' }, { value: 'smooth', label: 'Yumusak' }]"
|
||||
@update:model-value="(v) => updateStyle('curveType', v)"
|
||||
/>
|
||||
<PropCheckbox
|
||||
label="Noktalar"
|
||||
:model-value="element.style.showPoints ?? true"
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import PropCheckbox from './shared/PropCheckbox.vue'
|
||||
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||
import type { CheckboxElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: CheckboxElement }>()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
const booleanFields = computed(() =>
|
||||
schemaStore.scalarFields.filter((f) => f.type === 'boolean' || f.type === 'string'),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropSection title="Onay Kutusu">
|
||||
<PropFieldSelect
|
||||
label="Veri Alani"
|
||||
:model-value="element.binding?.path ?? ''"
|
||||
:fields="booleanFields"
|
||||
:allow-empty="true"
|
||||
empty-label="Yok (statik)"
|
||||
data-tip="Onay durumunun gelecegi veri alani"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
update({
|
||||
binding: v ? { type: 'scalar', path: v } : undefined,
|
||||
checked: v ? undefined : element.checked ?? false,
|
||||
} as any)
|
||||
"
|
||||
/>
|
||||
<PropCheckbox
|
||||
v-if="!element.binding"
|
||||
label="Isaretli"
|
||||
@@ -40,5 +63,13 @@ const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
data-tip="Kutu kenarlik rengi"
|
||||
@update:model-value="(v) => updateStyle('borderColor', v)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Kenar Kalinligi"
|
||||
:model-value="element.style.borderWidth ?? 0.3"
|
||||
:step="0.1"
|
||||
:min="0"
|
||||
data-tip="Kutu kenarlik kalinligi (mm)"
|
||||
@update:model-value="(v) => updateStyle('borderWidth', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -29,10 +29,13 @@ const formatOptions = [
|
||||
/>
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 10"
|
||||
:font-weight="style().fontWeight ?? 'normal'"
|
||||
:font-family="style().fontFamily"
|
||||
:color="style().color ?? '#666666'"
|
||||
:align="style().align ?? 'left'"
|
||||
:show-weight="false"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
|
||||
@@ -29,10 +29,13 @@ const formatOptions = [
|
||||
/>
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 10"
|
||||
:font-weight="style().fontWeight ?? 'normal'"
|
||||
:font-family="style().fontFamily"
|
||||
:color="style().color ?? '#666666'"
|
||||
:align="style().align ?? 'center'"
|
||||
:show-weight="false"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||
import type { RichTextElement, RichTextSpan, TextStyle } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: RichTextElement }>()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
function updateSpan(index: number, updates: Partial<RichTextSpan>) {
|
||||
const content = [...props.element.content]
|
||||
@@ -43,10 +46,13 @@ const weightOptions = [
|
||||
<PropSection title="Varsayilan Stil">
|
||||
<PropTextStyleGroup
|
||||
:font-size="element.style.fontSize ?? 11"
|
||||
:font-weight="element.style.fontWeight ?? 'normal'"
|
||||
:font-family="element.style.fontFamily"
|
||||
:color="element.style.color ?? '#000000'"
|
||||
:align="element.style.align ?? 'left'"
|
||||
:show-weight="false"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
@@ -79,6 +85,15 @@ const weightOptions = [
|
||||
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })"
|
||||
/>
|
||||
</div>
|
||||
<PropFieldSelect
|
||||
label="Binding"
|
||||
:model-value="span.binding?.path ?? ''"
|
||||
:fields="schemaStore.scalarFields"
|
||||
:allow-empty="true"
|
||||
empty-label="Yok (statik)"
|
||||
data-tip="Span'in baglanacagi veri alani"
|
||||
@update:model-value="(v) => updateSpan(idx, { binding: v ? { type: 'scalar', path: v } : undefined })"
|
||||
/>
|
||||
<div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input
|
||||
@@ -109,6 +124,13 @@ const weightOptions = [
|
||||
data-tip="Span metin rengi"
|
||||
@update:model-value="(v) => updateSpanStyle(idx, 'color', v)"
|
||||
/>
|
||||
<PropSelect
|
||||
label="Hizalama"
|
||||
:model-value="(span.style as TextStyle).align ?? ''"
|
||||
:options="[{ value: '', label: 'Varsayilan' }, { value: 'left', label: 'Sol' }, { value: 'center', label: 'Orta' }, { value: 'right', label: 'Sag' }]"
|
||||
data-tip="Span hizalamasi"
|
||||
@update:model-value="(v) => updateSpanStyle(idx, 'align', v || undefined)"
|
||||
/>
|
||||
</div>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,12 @@ const shapeOptions = [
|
||||
{ value: 'rounded_rectangle', label: 'Yuvarlak Dikdortgen' },
|
||||
{ value: 'ellipse', label: 'Elips' },
|
||||
]
|
||||
|
||||
const borderStyleOptions = [
|
||||
{ value: 'solid', label: 'Duz' },
|
||||
{ value: 'dashed', label: 'Kesikli' },
|
||||
{ value: 'dotted', label: 'Noktali' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -46,6 +52,13 @@ const shapeOptions = [
|
||||
data-tip="Kenarlik cizgi kalinligi (mm)"
|
||||
@update:model-value="(v) => updateStyle('borderWidth', v)"
|
||||
/>
|
||||
<PropSelect
|
||||
label="Kenar Stili"
|
||||
:model-value="element.style.borderStyle ?? 'solid'"
|
||||
:options="borderStyleOptions"
|
||||
data-tip="Kenarlik cizgi stili"
|
||||
@update:model-value="(v) => updateStyle('borderStyle', v)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
v-if="element.shapeType === 'rounded_rectangle'"
|
||||
label="Kose Yuvarlakligi"
|
||||
|
||||
@@ -18,6 +18,10 @@ function updateSize(axis: 'width' | 'height', sv: SizeValue) {
|
||||
templateStore.updateElementSize(props.element.id, { [axis]: sv })
|
||||
}
|
||||
|
||||
function updateSizeConstraint(key: string, value: number | undefined) {
|
||||
templateStore.updateElementSize(props.element.id, { [key]: value })
|
||||
}
|
||||
|
||||
function onTypeChange(axis: 'width' | 'height', type: string) {
|
||||
if (type === 'auto') updateSize(axis, { type: 'auto' })
|
||||
else if (type === 'fr') updateSize(axis, { type: 'fr', value: 1 })
|
||||
@@ -79,5 +83,49 @@ function onTypeChange(axis: 'width' | 'height', type: string) {
|
||||
data-tip="Sabit yukseklik degeri (mm)"
|
||||
@update:model-value="(v) => updateSize('height', { type: 'fixed', value: v })"
|
||||
/>
|
||||
<PropNumberInput
|
||||
v-if="element.size.height.type === 'fr'"
|
||||
label="fr"
|
||||
:model-value="(element.size.height as any).value"
|
||||
:step="1"
|
||||
:min="1"
|
||||
data-tip="Kalan alani oransal doldurma degeri"
|
||||
@update:model-value="(v) => updateSize('height', { type: 'fr', value: v })"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<PropSection title="Min / Max">
|
||||
<PropNumberInput
|
||||
label="Min Gen."
|
||||
:model-value="element.size.minWidth ?? 0"
|
||||
:step="1"
|
||||
:min="0"
|
||||
data-tip="Minimum genislik (mm) — 0 = sinir yok"
|
||||
@update:model-value="(v) => updateSizeConstraint('minWidth', v > 0 ? v : undefined)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Max Gen."
|
||||
:model-value="element.size.maxWidth ?? 0"
|
||||
:step="1"
|
||||
:min="0"
|
||||
data-tip="Maksimum genislik (mm) — 0 = sinir yok"
|
||||
@update:model-value="(v) => updateSizeConstraint('maxWidth', v > 0 ? v : undefined)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Min Yuk."
|
||||
:model-value="element.size.minHeight ?? 0"
|
||||
:step="1"
|
||||
:min="0"
|
||||
data-tip="Minimum yukseklik (mm) — 0 = sinir yok"
|
||||
@update:model-value="(v) => updateSizeConstraint('minHeight', v > 0 ? v : undefined)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Max Yuk."
|
||||
:model-value="element.size.maxHeight ?? 0"
|
||||
:step="1"
|
||||
:min="0"
|
||||
data-tip="Maksimum yukseklik (mm) — 0 = sinir yok"
|
||||
@update:model-value="(v) => updateSizeConstraint('maxHeight', v > 0 ? v : undefined)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||
import type { StaticTextElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import type { StaticTextElement, TextElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const schemaStore = useSchemaStore()
|
||||
const style = () => props.element.style as TextStyle
|
||||
|
||||
const isText = computed(() => props.element.type === 'text')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropSection title="Metin Stili">
|
||||
<PropSection title="Metin">
|
||||
<div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
|
||||
<label class="prop-label">Metin</label>
|
||||
<input
|
||||
@@ -21,13 +27,38 @@ const style = () => props.element.style as TextStyle
|
||||
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="isText">
|
||||
<PropFieldSelect
|
||||
label="Veri Alani"
|
||||
:model-value="(element as TextElement).binding?.path ?? ''"
|
||||
:fields="schemaStore.scalarFields"
|
||||
data-tip="Metnin baglanacagi veri alani"
|
||||
@update:model-value="(v) => update({ binding: { type: 'scalar', path: v } } as any)"
|
||||
/>
|
||||
<div class="prop-row" data-tip="Veri alaninin onune eklenecek sabit metin">
|
||||
<label class="prop-label">Ön Ek</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="(element as TextElement).content ?? ''"
|
||||
placeholder="ör: Fatura No: "
|
||||
@input="(e) => update({ content: (e.target as HTMLInputElement).value || undefined } as any)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</PropSection>
|
||||
|
||||
<PropSection title="Metin Stili">
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 11"
|
||||
:font-weight="style().fontWeight ?? 'normal'"
|
||||
:font-family="style().fontFamily"
|
||||
:color="style().color ?? '#000000'"
|
||||
:align="style().align ?? 'left'"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
|
||||
84
frontend/src/components/properties/shared/PropCondition.vue
Normal file
84
frontend/src/components/properties/shared/PropCondition.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useSchemaStore } from '../../../stores/schema'
|
||||
import PropFieldSelect from './PropFieldSelect.vue'
|
||||
import PropSelect from './PropSelect.vue'
|
||||
import PropSection from './PropSection.vue'
|
||||
import type { Condition } from '../../../core/types'
|
||||
import '../../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{
|
||||
condition?: Condition
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:condition': [value: Condition | undefined]
|
||||
}>()
|
||||
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
const enabled = computed(() => !!props.condition)
|
||||
|
||||
const operatorOptions = [
|
||||
{ value: 'eq', label: '= Esit' },
|
||||
{ value: 'neq', label: '≠ Esit Degil' },
|
||||
{ value: 'gt', label: '> Buyuk' },
|
||||
{ value: 'gte', label: '>= Buyuk Esit' },
|
||||
{ value: 'lt', label: '< Kucuk' },
|
||||
{ value: 'lte', label: '<= Kucuk Esit' },
|
||||
{ value: 'truthy', label: 'Dolu (truthy)' },
|
||||
{ value: 'falsy', label: 'Bos (falsy)' },
|
||||
]
|
||||
|
||||
const needsValue = computed(() => {
|
||||
const op = props.condition?.operator
|
||||
return op && op !== 'truthy' && op !== 'falsy'
|
||||
})
|
||||
|
||||
function toggle(on: boolean) {
|
||||
if (on) {
|
||||
emit('update:condition', { path: '', operator: 'truthy' })
|
||||
} else {
|
||||
emit('update:condition', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function updateField(key: keyof Condition, value: unknown) {
|
||||
emit('update:condition', { ...props.condition!, [key]: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropSection title="Kosullu Gosterim">
|
||||
<div class="prop-row" data-tip="Elemani belirli bir kosulla goster/gizle">
|
||||
<label class="prop-label">Aktif</label>
|
||||
<input type="checkbox" :checked="enabled" @change="toggle(($event.target as HTMLInputElement).checked)" />
|
||||
</div>
|
||||
|
||||
<template v-if="enabled">
|
||||
<PropFieldSelect
|
||||
label="Alan"
|
||||
:model-value="condition!.path"
|
||||
:fields="schemaStore.scalarFields"
|
||||
data-tip="Kosulun degerlendirilecegi veri alani"
|
||||
@update:model-value="(v) => updateField('path', v)"
|
||||
/>
|
||||
<PropSelect
|
||||
label="Operator"
|
||||
:model-value="condition!.operator"
|
||||
:options="operatorOptions"
|
||||
data-tip="Karsilastirma operatoru"
|
||||
@update:model-value="(v) => updateField('operator', v)"
|
||||
/>
|
||||
<div v-if="needsValue" class="prop-row" data-tip="Karsilastirilacak deger">
|
||||
<label class="prop-label">Deger</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="condition!.value ?? ''"
|
||||
@input="(e) => updateField('value', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</PropSection>
|
||||
</template>
|
||||
@@ -1,26 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTemplateStore } from '../../../stores/template'
|
||||
import PropNumberInput from './PropNumberInput.vue'
|
||||
import PropColorInput from './PropColorInput.vue'
|
||||
import PropSelect from './PropSelect.vue'
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
fontSize: number
|
||||
fontWeight?: string
|
||||
fontFamily?: string
|
||||
color: string
|
||||
align: string
|
||||
showWeight?: boolean
|
||||
}>(),
|
||||
{ fontWeight: 'normal', showWeight: true },
|
||||
{ fontWeight: 'normal', fontFamily: undefined, showWeight: true },
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
'update:fontSize': [value: number]
|
||||
'update:fontWeight': [value: string]
|
||||
'update:fontFamily': [value: string | undefined]
|
||||
'update:color': [value: string]
|
||||
'update:align': [value: string]
|
||||
}>()
|
||||
|
||||
const templateStore = useTemplateStore()
|
||||
|
||||
const fontOptions = computed(() =>
|
||||
templateStore.template.fonts.map((f) => ({ value: f, label: f })),
|
||||
)
|
||||
|
||||
const weightOptions = [
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'bold', label: 'Kalin' },
|
||||
@@ -34,6 +44,14 @@ const alignOptions = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropSelect
|
||||
v-if="fontOptions.length > 1"
|
||||
label="Font"
|
||||
:model-value="fontFamily ?? fontOptions[0]?.value ?? ''"
|
||||
:options="fontOptions"
|
||||
data-tip="Yazi tipi ailesi"
|
||||
@update:model-value="$emit('update:fontFamily', $event)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Boyut (pt)"
|
||||
:model-value="fontSize"
|
||||
|
||||
@@ -81,7 +81,25 @@ const emit = defineEmits<{
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-clbl">Zebra</span>
|
||||
<span class="ts-clbl">Tek</span>
|
||||
</div>
|
||||
<div class="ts-color-item" data-tip="Zebra satir rengi — cift satirlar">
|
||||
<div class="ts-swatch-wrap">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="style.zebraEven ?? '#ffffff'"
|
||||
@input="(e) => emit('update:style', 'zebraEven', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="style.zebraEven"
|
||||
class="ts-swatch-clr"
|
||||
@click="emit('update:style', 'zebraEven', undefined)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-clbl">Cift</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -250,11 +250,22 @@ export interface ChartLabels {
|
||||
color?: string
|
||||
}
|
||||
|
||||
export interface ChartReferenceLine {
|
||||
categoryIndex: number
|
||||
color?: string
|
||||
width?: number
|
||||
label?: string
|
||||
dash?: boolean
|
||||
}
|
||||
|
||||
export interface ChartAxis {
|
||||
xLabel?: string
|
||||
yLabel?: string
|
||||
showGrid?: boolean
|
||||
gridColor?: string
|
||||
showVerticalGrid?: boolean
|
||||
verticalGridColor?: string
|
||||
referenceLines?: ChartReferenceLine[]
|
||||
}
|
||||
|
||||
export interface ChartStyle {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
4
justfile
4
justfile
@@ -55,6 +55,10 @@ test-visual-editor:
|
||||
test-visual: visual-refs
|
||||
cd frontend && bun run test:visual
|
||||
|
||||
# Visual snapshot'lari guncelle (UI degisikliklerinden sonra)
|
||||
update-snapshots: visual-refs
|
||||
cd frontend && bun run test:visual -- --update-snapshots
|
||||
|
||||
# Tum testler (Rust + frontend unit + visual)
|
||||
test-all: test-rust test-front test-visual
|
||||
|
||||
|
||||
@@ -129,10 +129,23 @@ pub struct LineChartLayout {
|
||||
pub show_labels: bool,
|
||||
pub label_font: f64,
|
||||
pub label_color: String,
|
||||
pub smooth: bool,
|
||||
/// X axis line endpoints
|
||||
pub x_axis_y: f64,
|
||||
pub x_axis_x1: f64,
|
||||
pub x_axis_x2: f64,
|
||||
/// Vertical reference lines
|
||||
pub ref_lines: Vec<RefLineLayout>,
|
||||
}
|
||||
|
||||
pub struct RefLineLayout {
|
||||
pub x: f64,
|
||||
pub y1: f64,
|
||||
pub y2: f64,
|
||||
pub color: String,
|
||||
pub width: f64,
|
||||
pub dash: bool,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
pub struct PieSlice {
|
||||
@@ -223,6 +236,10 @@ pub trait ChartDataSource {
|
||||
fn inner_radius(&self) -> Option<f64>;
|
||||
fn show_points(&self) -> Option<bool>;
|
||||
fn line_width(&self) -> Option<f64>;
|
||||
fn curve_type(&self) -> Option<&str>;
|
||||
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine];
|
||||
fn show_vertical_grid(&self) -> bool;
|
||||
fn vertical_grid_color(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -314,6 +331,18 @@ impl ChartDataSource for crate::data_resolve::ResolvedChartData {
|
||||
fn line_width(&self) -> Option<f64> {
|
||||
self.style.line_width
|
||||
}
|
||||
fn curve_type(&self) -> Option<&str> {
|
||||
self.style.curve_type.as_deref()
|
||||
}
|
||||
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine] {
|
||||
self.axis.as_ref().map_or(&[], |a| &a.reference_lines)
|
||||
}
|
||||
fn show_vertical_grid(&self) -> bool {
|
||||
self.axis.as_ref().and_then(|a| a.show_vertical_grid).unwrap_or(true)
|
||||
}
|
||||
fn vertical_grid_color(&self) -> Option<&str> {
|
||||
self.axis.as_ref().and_then(|a| a.vertical_grid_color.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -403,6 +432,18 @@ impl ChartDataSource for crate::ChartRenderData {
|
||||
fn line_width(&self) -> Option<f64> {
|
||||
self.line_width
|
||||
}
|
||||
fn curve_type(&self) -> Option<&str> {
|
||||
self.curve_type.as_deref()
|
||||
}
|
||||
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine] {
|
||||
&self.reference_lines
|
||||
}
|
||||
fn show_vertical_grid(&self) -> bool {
|
||||
self.show_vertical_grid
|
||||
}
|
||||
fn vertical_grid_color(&self) -> Option<&str> {
|
||||
self.vertical_grid_color.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -693,22 +734,14 @@ pub fn compute_x_labels_line(
|
||||
rotate_angle: 0.0,
|
||||
};
|
||||
}
|
||||
let spacing = if n_cats == 1 {
|
||||
pw
|
||||
} else {
|
||||
pw / (n_cats - 1) as f64
|
||||
};
|
||||
let step = pw / n_cats as f64;
|
||||
let max_label_len = categories.iter().map(|c| c.len()).max().unwrap_or(0);
|
||||
let rotate_angle = compute_label_rotation(max_label_len, spacing);
|
||||
let rotate_angle = compute_label_rotation(max_label_len, step);
|
||||
let labels = categories
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ci, cat)| {
|
||||
let x = if n_cats == 1 {
|
||||
px + pw / 2.0
|
||||
} else {
|
||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
||||
};
|
||||
let x = px + step / 2.0 + ci as f64 * step;
|
||||
XLabel {
|
||||
text: cat.clone(),
|
||||
x,
|
||||
@@ -833,6 +866,11 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
let show_labels = data.show_labels();
|
||||
let label_font = data.label_font_size().unwrap_or(2.2);
|
||||
let label_color = data.label_color().unwrap_or("#333").to_string();
|
||||
let smooth = data.curve_type() == Some("smooth");
|
||||
|
||||
// Slot-based positioning: each category gets a slot, point centered in slot
|
||||
// This adds padding on left/right so first/last points don't touch axes
|
||||
let step = if n_cats > 0 { pw / n_cats as f64 } else { pw };
|
||||
|
||||
let series = (0..data.series_count())
|
||||
.map(|si| {
|
||||
@@ -841,11 +879,7 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ci, val)| {
|
||||
let x = if n_cats == 1 {
|
||||
px + pw / 2.0
|
||||
} else {
|
||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
||||
};
|
||||
let x = px + step / 2.0 + ci as f64 * step;
|
||||
let y = py + ph - ((val - min_val) / range) * ph;
|
||||
LinePoint { x, y, value: *val }
|
||||
})
|
||||
@@ -859,6 +893,42 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
|
||||
let x_labels = compute_x_labels_line(data.categories(), px, py + ph, pw);
|
||||
|
||||
// Vertical grid lines at each category
|
||||
let vgrid_color = data.vertical_grid_color().unwrap_or("#E5E7EB").to_string();
|
||||
let mut ref_lines: Vec<RefLineLayout> = if data.show_vertical_grid() {
|
||||
(0..n_cats).map(|ci| {
|
||||
let x = px + step / 2.0 + ci as f64 * step;
|
||||
RefLineLayout {
|
||||
x,
|
||||
y1: py,
|
||||
y2: py + ph,
|
||||
color: vgrid_color.clone(),
|
||||
width: 0.15,
|
||||
dash: false,
|
||||
label: None,
|
||||
}
|
||||
}).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Explicit reference lines (overlay on top of grid)
|
||||
for rl in data.reference_lines() {
|
||||
if rl.category_index >= n_cats {
|
||||
continue;
|
||||
}
|
||||
let x = px + step / 2.0 + rl.category_index as f64 * step;
|
||||
ref_lines.push(RefLineLayout {
|
||||
x,
|
||||
y1: py,
|
||||
y2: py + ph,
|
||||
color: rl.color.clone().unwrap_or_else(|| "#9CA3AF".to_string()),
|
||||
width: rl.width.unwrap_or(0.3),
|
||||
dash: rl.dash.unwrap_or(true),
|
||||
label: rl.label.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
LineChartLayout {
|
||||
min_val,
|
||||
max_val,
|
||||
@@ -870,9 +940,11 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
show_labels,
|
||||
label_font,
|
||||
label_color,
|
||||
smooth,
|
||||
x_axis_y: py + ph,
|
||||
x_axis_x1: px,
|
||||
x_axis_x2: px + pw,
|
||||
ref_lines,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,14 +121,13 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
// Y axis
|
||||
render_y_axis_svg(svg, &ll.y_axis);
|
||||
|
||||
let mut label_texts = String::new();
|
||||
|
||||
for series_layout in &ll.series {
|
||||
let color = color_at(&cl.palette, series_layout.color_idx);
|
||||
let mut points = String::new();
|
||||
let mut point_circles = String::new();
|
||||
|
||||
for pt in &series_layout.points {
|
||||
write!(points, "{:.2},{:.2} ", pt.x, pt.y).unwrap();
|
||||
|
||||
if ll.show_points {
|
||||
write!(
|
||||
point_circles,
|
||||
@@ -139,19 +138,75 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
}
|
||||
|
||||
if ll.show_labels {
|
||||
svg_text(svg, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
|
||||
svg_text(&mut label_texts, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
|
||||
}
|
||||
}
|
||||
|
||||
if ll.smooth && series_layout.points.len() >= 2 {
|
||||
// Catmull-Rom → cubic bezier smooth curve
|
||||
let pts = &series_layout.points;
|
||||
let mut d = format!("M{:.2},{:.2}", pts[0].x, pts[0].y);
|
||||
for i in 0..pts.len() - 1 {
|
||||
let p0 = if i > 0 { &pts[i - 1] } else { &pts[i] };
|
||||
let p1 = &pts[i];
|
||||
let p2 = &pts[i + 1];
|
||||
let p3 = if i + 2 < pts.len() { &pts[i + 2] } else { &pts[i + 1] };
|
||||
|
||||
let cp1x = p1.x + (p2.x - p0.x) / 6.0;
|
||||
let cp1y = p1.y + (p2.y - p0.y) / 6.0;
|
||||
let cp2x = p2.x - (p3.x - p1.x) / 6.0;
|
||||
let cp2y = p2.y - (p3.y - p1.y) / 6.0;
|
||||
|
||||
write!(d, " C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
|
||||
cp1x, cp1y, cp2x, cp2y, p2.x, p2.y
|
||||
).unwrap();
|
||||
}
|
||||
write!(
|
||||
svg,
|
||||
r##"<path d="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||
d, color, ll.line_width
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
let mut points = String::new();
|
||||
for pt in &series_layout.points {
|
||||
write!(points, "{:.2},{:.2} ", pt.x, pt.y).unwrap();
|
||||
}
|
||||
write!(
|
||||
svg,
|
||||
r##"<polyline points="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||
points.trim(), color, ll.line_width
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
svg.push_str(&point_circles);
|
||||
}
|
||||
|
||||
// Data labels (rendered after lines/points so they appear on top)
|
||||
svg.push_str(&label_texts);
|
||||
|
||||
// Reference lines (vertical)
|
||||
for rl in &ll.ref_lines {
|
||||
if rl.dash {
|
||||
write!(
|
||||
svg,
|
||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="{:.2}" stroke-dasharray="1.5,1"/>"##,
|
||||
rl.x, rl.y1, rl.x, rl.y2, rl.color, rl.width
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(
|
||||
svg,
|
||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="{:.2}"/>"##,
|
||||
rl.x, rl.y1, rl.x, rl.y2, rl.color, rl.width
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if let Some(ref label) = rl.label {
|
||||
svg_text(svg, rl.x, rl.y1 - 1.0, 2.0, &rl.color, SvgAnchor::Middle, label);
|
||||
}
|
||||
}
|
||||
|
||||
// X axis labels
|
||||
render_x_labels_svg(svg, &ll.x_labels);
|
||||
|
||||
@@ -560,6 +615,9 @@ mod tests {
|
||||
y_label: Some("Revenue".to_string()),
|
||||
show_grid: None,
|
||||
grid_color: None,
|
||||
show_vertical_grid: None,
|
||||
vertical_grid_color: None,
|
||||
reference_lines: vec![],
|
||||
});
|
||||
let svg = render_svg(&data, 100.0, 60.0);
|
||||
|
||||
@@ -575,6 +633,9 @@ mod tests {
|
||||
y_label: Some("Y Label".to_string()),
|
||||
show_grid: None,
|
||||
grid_color: None,
|
||||
show_vertical_grid: None,
|
||||
vertical_grid_color: None,
|
||||
reference_lines: vec![],
|
||||
});
|
||||
let svg = render_svg(&data, 80.0, 80.0);
|
||||
|
||||
|
||||
@@ -161,8 +161,21 @@ pub struct ChartRenderData {
|
||||
// Title align
|
||||
#[serde(default)]
|
||||
pub title_align: Option<String>,
|
||||
// Curve type for line charts
|
||||
#[serde(default)]
|
||||
pub curve_type: Option<String>,
|
||||
// Vertical reference lines
|
||||
#[serde(default)]
|
||||
pub reference_lines: Vec<dreport_core::models::ChartReferenceLine>,
|
||||
// Vertical grid
|
||||
#[serde(default = "default_true")]
|
||||
pub show_vertical_grid: bool,
|
||||
#[serde(default)]
|
||||
pub vertical_grid_color: Option<String>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChartSeriesData {
|
||||
pub name: String,
|
||||
@@ -340,6 +353,10 @@ impl From<&data_resolve::ResolvedChartData> for ChartRenderData {
|
||||
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
|
||||
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
|
||||
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
|
||||
curve_type: cd.style.curve_type.clone(),
|
||||
reference_lines: cd.axis.as_ref().map_or_else(Vec::new, |a| a.reference_lines.clone()),
|
||||
show_vertical_grid: cd.axis.as_ref().and_then(|a| a.show_vertical_grid).unwrap_or(true),
|
||||
vertical_grid_color: cd.axis.as_ref().and_then(|a| a.vertical_grid_color.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1015,6 +1015,27 @@ fn render_chart(
|
||||
}));
|
||||
let path = {
|
||||
let mut pb = PathBuilder::new();
|
||||
if ll.smooth && points.len() >= 2 {
|
||||
let pts = &series_layout.points;
|
||||
pb.move_to(mm(pts[0].x), mm(pts[0].y));
|
||||
for i in 0..pts.len() - 1 {
|
||||
let p0 = if i > 0 { &pts[i - 1] } else { &pts[i] };
|
||||
let p1 = &pts[i];
|
||||
let p2 = &pts[i + 1];
|
||||
let p3 = if i + 2 < pts.len() { &pts[i + 2] } else { &pts[i + 1] };
|
||||
|
||||
let cp1x = p1.x + (p2.x - p0.x) / 6.0;
|
||||
let cp1y = p1.y + (p2.y - p0.y) / 6.0;
|
||||
let cp2x = p2.x - (p3.x - p1.x) / 6.0;
|
||||
let cp2y = p2.y - (p3.y - p1.y) / 6.0;
|
||||
|
||||
pb.cubic_to(
|
||||
mm(cp1x), mm(cp1y),
|
||||
mm(cp2x), mm(cp2y),
|
||||
mm(p2.x), mm(p2.y),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (i, (lx, ly)) in points.iter().enumerate() {
|
||||
if i == 0 {
|
||||
pb.move_to(mm(*lx), mm(*ly));
|
||||
@@ -1022,6 +1043,7 @@ fn render_chart(
|
||||
pb.line_to(mm(*lx), mm(*ly));
|
||||
}
|
||||
}
|
||||
}
|
||||
pb.finish()
|
||||
};
|
||||
if let Some(p) = path {
|
||||
@@ -1055,6 +1077,32 @@ fn render_chart(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reference lines (vertical)
|
||||
for rl in &ll.ref_lines {
|
||||
let rl_color = parse_color(&rl.color);
|
||||
chart_line_seg(
|
||||
surface,
|
||||
rl.x,
|
||||
rl.y1,
|
||||
rl.x,
|
||||
rl.y2,
|
||||
rl_color,
|
||||
(rl.width * 2.5) as f32,
|
||||
);
|
||||
if let Some(ref label) = rl.label {
|
||||
chart_text(
|
||||
surface,
|
||||
rl.x,
|
||||
rl.y1 - 1.0,
|
||||
label,
|
||||
2.0,
|
||||
&rl.color,
|
||||
ChartTextAlign::Center,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
}
|
||||
}
|
||||
render_chart_x_labels(surface, &ll.x_labels, fonts, measurer);
|
||||
let ac = parse_color("#9CA3AF");
|
||||
chart_line_seg(
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user