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
|
||||
widthMm: number; heightMm: number; bleedMm: number; safeMm: number
|
||||
front: El[]; back: El[]
|
||||
hasBackground?: boolean; fonts?: string[]
|
||||
}
|
||||
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 }
|
||||
@ -168,6 +169,38 @@ async function save() {
|
||||
saved.value = true
|
||||
} 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() {
|
||||
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' })
|
||||
@ -210,11 +243,30 @@ onMounted(load)
|
||||
</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">
|
||||
<!-- Beschnitt-Fläche -->
|
||||
<div class="bleed" :style="{ width: (tpl.widthMm + 2 * tpl.bleedMm) * SCALE + 'px', height: (tpl.heightMm + 2 * tpl.bleedMm) * SCALE + 'px' }">
|
||||
<!-- 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">
|
||||
<!-- 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 -->
|
||||
<div class="safe" :style="{ inset: tpl.safeMm * SCALE + 'px' }"></div>
|
||||
<!-- Elemente -->
|
||||
@ -273,6 +325,15 @@ onMounted(load)
|
||||
</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 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>
|
||||
|
||||
<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); }
|
||||
.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; }
|
||||
.bleed { position: relative; background: #fbe9da; box-shadow: var(--shadow-sm); }
|
||||
.trim { position: absolute; background: #fff; overflow: hidden; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user