Karten-Editor: Upload-UI für Hintergrund-PDF & Schriften
- Assets-Leiste: Hintergrund-PDF hochladen/entfernen (+ "aktiv"-Badge), Schrift hochladen (TTF/OTF) mit Familien-Chips - Schriftart-Auswahl pro Text/Feld (Helvetica/Times/Courier + eigene) - Canvas-Hinweis bei aktivem Hintergrund (echte Darstellung in PDF-Vorschau) - Uploads aktualisieren State ohne Verlust des aktuellen Layouts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b52d696cc5
commit
f25ccefa48
@ -8,6 +8,7 @@ interface Template {
|
|||||||
id: string | null; isDefault: boolean; name: string
|
id: string | null; isDefault: boolean; name: string
|
||||||
widthMm: number; heightMm: number; bleedMm: number; safeMm: number
|
widthMm: number; heightMm: number; bleedMm: number; safeMm: number
|
||||||
front: El[]; back: El[]
|
front: El[]; back: El[]
|
||||||
|
hasBackground?: boolean; fonts?: string[]
|
||||||
}
|
}
|
||||||
interface Company { '@id': string; id: string; name: string; slug: string; brandingConfig: Record<string, string> | unknown[] }
|
interface Company { '@id': string; id: string; name: string; slug: string; brandingConfig: Record<string, string> | unknown[] }
|
||||||
interface Employee { id: string; firstName: string; lastName: string; position: string | null; email: string | null; phone: string | null; mobile: string | null; company: string; department: string | null }
|
interface Employee { id: string; firstName: string; lastName: string; position: string | null; email: string | null; phone: string | null; mobile: string | null; company: string; department: string | null }
|
||||||
@ -168,6 +169,38 @@ async function save() {
|
|||||||
saved.value = true
|
saved.value = true
|
||||||
} finally { saving.value = false }
|
} finally { saving.value = false }
|
||||||
}
|
}
|
||||||
|
// --- Druck-Assets: Hintergrund-PDF & Schriften ---
|
||||||
|
const uploading = ref(false)
|
||||||
|
function cidPath() { return selectedCompany.value?.id }
|
||||||
|
|
||||||
|
async function uploadBackground(e: Event) {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
|
if (!file || !tpl.value) return
|
||||||
|
uploading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData(); fd.append('file', file)
|
||||||
|
await client.post(`/companies/${cidPath()}/card-template/background`, fd)
|
||||||
|
tpl.value.hasBackground = true
|
||||||
|
} catch { alert('Upload fehlgeschlagen (nur PDF).') } finally { uploading.value = false; (e.target as HTMLInputElement).value = '' }
|
||||||
|
}
|
||||||
|
async function removeBackground() {
|
||||||
|
if (!tpl.value || !confirm('Hintergrund-PDF entfernen?')) return
|
||||||
|
await client.delete(`/companies/${cidPath()}/card-template/background`)
|
||||||
|
tpl.value.hasBackground = false
|
||||||
|
}
|
||||||
|
async function uploadFont(e: Event) {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
|
if (!file || !tpl.value) return
|
||||||
|
const family = (prompt('Name der Schriftart (z. B. „Brand")', file.name.replace(/\.(ttf|otf)$/i, '')) || '').trim()
|
||||||
|
if (!family) { (e.target as HTMLInputElement).value = ''; return }
|
||||||
|
uploading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData(); fd.append('file', file); fd.append('family', family)
|
||||||
|
await client.post(`/companies/${cidPath()}/card-template/font`, fd)
|
||||||
|
tpl.value.fonts = [...(tpl.value.fonts ?? []).filter((f) => f !== family), family]
|
||||||
|
} catch { alert('Upload fehlgeschlagen (nur TTF/OTF).') } finally { uploading.value = false; (e.target as HTMLInputElement).value = '' }
|
||||||
|
}
|
||||||
|
|
||||||
async function previewPdf() {
|
async function previewPdf() {
|
||||||
if (!sample.value) { alert('Kein Mitarbeiter in dieser Firma für die Vorschau.'); return }
|
if (!sample.value) { alert('Kein Mitarbeiter in dieser Firma für die Vorschau.'); return }
|
||||||
const res = await client.get(`/employees/${sample.value.id}/card.pdf`, { responseType: 'blob' })
|
const res = await client.get(`/employees/${sample.value.id}/card.pdf`, { responseType: 'blob' })
|
||||||
@ -210,11 +243,30 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="assets">
|
||||||
|
<label class="btn btn-ghost btn-sm" :class="{ disabled: uploading }">
|
||||||
|
Hintergrund-PDF…
|
||||||
|
<input type="file" accept="application/pdf,.pdf" hidden @change="uploadBackground" />
|
||||||
|
</label>
|
||||||
|
<template v-if="tpl.hasBackground">
|
||||||
|
<span class="badge badge-active">Hintergrund aktiv</span>
|
||||||
|
<button class="btn btn-ghost btn-sm" @click="removeBackground">Entfernen</button>
|
||||||
|
</template>
|
||||||
|
<span class="sep"></span>
|
||||||
|
<label class="btn btn-ghost btn-sm" :class="{ disabled: uploading }">
|
||||||
|
Schrift…
|
||||||
|
<input type="file" accept=".ttf,.otf" hidden @change="uploadFont" />
|
||||||
|
</label>
|
||||||
|
<span v-for="f in (tpl.fonts || [])" :key="f" class="chip">{{ f }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="stage">
|
<div class="stage">
|
||||||
<!-- Beschnitt-Fläche -->
|
<!-- Beschnitt-Fläche -->
|
||||||
<div class="bleed" :style="{ width: (tpl.widthMm + 2 * tpl.bleedMm) * SCALE + 'px', height: (tpl.heightMm + 2 * tpl.bleedMm) * SCALE + 'px' }">
|
<div class="bleed" :style="{ width: (tpl.widthMm + 2 * tpl.bleedMm) * SCALE + 'px', height: (tpl.heightMm + 2 * tpl.bleedMm) * SCALE + 'px' }">
|
||||||
<!-- Endformat (Trim) -->
|
<!-- Endformat (Trim) -->
|
||||||
<div class="trim" :style="{ left: tpl.bleedMm * SCALE + 'px', top: tpl.bleedMm * SCALE + 'px', width: tpl.widthMm * SCALE + 'px', height: tpl.heightMm * SCALE + 'px' }" @mousedown.self="selectedIndex = null">
|
<div class="trim" :style="{ left: tpl.bleedMm * SCALE + 'px', top: tpl.bleedMm * SCALE + 'px', width: tpl.widthMm * SCALE + 'px', height: tpl.heightMm * SCALE + 'px' }" @mousedown.self="selectedIndex = null">
|
||||||
|
<!-- Hinweis: Hintergrund-PDF aktiv (echtes Bild in der PDF-Vorschau) -->
|
||||||
|
<div v-if="tpl.hasBackground" class="bg-note">Hintergrund-PDF aktiv<br /><small>sichtbar in der PDF-Vorschau</small></div>
|
||||||
<!-- Sicherheitsabstand -->
|
<!-- Sicherheitsabstand -->
|
||||||
<div class="safe" :style="{ inset: tpl.safeMm * SCALE + 'px' }"></div>
|
<div class="safe" :style="{ inset: tpl.safeMm * SCALE + 'px' }"></div>
|
||||||
<!-- Elemente -->
|
<!-- Elemente -->
|
||||||
@ -273,6 +325,15 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
<div class="field"><label>Fett</label><select class="input" v-model="selected.bold"><option :value="false">Nein</option><option :value="true">Ja</option></select></div>
|
<div class="field"><label>Fett</label><select class="input" v-model="selected.bold"><option :value="false">Nein</option><option :value="true">Ja</option></select></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Schriftart</label>
|
||||||
|
<select class="input" v-model="selected.fontFamily">
|
||||||
|
<option value="">Standard (Helvetica)</option>
|
||||||
|
<option value="times">Times</option>
|
||||||
|
<option value="courier">Courier</option>
|
||||||
|
<option v-for="f in (tpl.fonts || [])" :key="f" :value="f">{{ f }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="field" v-if="selected.type !== 'qr' && selected.type !== 'image'">
|
<div class="field" v-if="selected.type !== 'qr' && selected.type !== 'image'">
|
||||||
@ -308,6 +369,12 @@ onMounted(load)
|
|||||||
.tab.active { background: var(--dark); color: #fff; border-color: var(--dark); }
|
.tab.active { background: var(--dark); color: #fff; border-color: var(--dark); }
|
||||||
.adds { display: flex; gap: .35rem; flex-wrap: wrap; }
|
.adds { display: flex; gap: .35rem; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
.assets { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: 1rem; padding: .7rem .9rem; background: #fff; border: 1px solid var(--line); border-radius: var(--radius-sm); }
|
||||||
|
.assets .btn.disabled { opacity: .6; pointer-events: none; }
|
||||||
|
.assets .sep { width: 1px; height: 20px; background: var(--line); margin: 0 .3rem; }
|
||||||
|
.chip { background: var(--psc-orange-soft); color: var(--psc-orange-dark); padding: .15rem .6rem; border-radius: 999px; font-size: .78rem; font-weight: 600; }
|
||||||
|
.bg-note { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; color: #b3b3b3; font-size: 12px; background: repeating-linear-gradient(45deg, #fafafa, #fafafa 8px, #f2f2f2 8px, #f2f2f2 16px); pointer-events: none; }
|
||||||
|
.bg-note small { font-size: 10px; }
|
||||||
.stage { background: #ececec; border-radius: var(--radius); padding: 1.5rem; display: flex; flex-direction: column; align-items: center; overflow-x: auto; }
|
.stage { background: #ececec; border-radius: var(--radius); padding: 1.5rem; display: flex; flex-direction: column; align-items: center; overflow-x: auto; }
|
||||||
.bleed { position: relative; background: #fbe9da; box-shadow: var(--shadow-sm); }
|
.bleed { position: relative; background: #fbe9da; box-shadow: var(--shadow-sm); }
|
||||||
.trim { position: absolute; background: #fff; overflow: hidden; }
|
.trim { position: absolute; background: #fff; overflow: hidden; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user