feat: implementiert PDF-Generierung mit Hintergrundbild und Dokumentation
This commit is contained in:
@@ -81,8 +81,8 @@ else
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-3 mt-5 no-print">
|
||||
<button type="button" class="btn btn-primary btn-lg px-4 shadow-sm" @onclick="PrintPage">
|
||||
<i class="oi oi-print me-2"></i> Als PDF speichern
|
||||
<button type="button" class="btn btn-primary btn-lg px-4 shadow-sm" @onclick="ShowPdfPreview">
|
||||
<i class="oi oi-eye me-2"></i> PDF Vorschau
|
||||
</button>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_item.Link))
|
||||
@@ -134,6 +134,28 @@ else
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (_showPdfModal)
|
||||
{
|
||||
<div class="modal fade show" style="display: block; background: rgba(0,0,0,0.6); z-index: 1050;" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered" style="max-width: 90vw; height: 90vh;">
|
||||
<div class="modal-content" style="height: 90vh;">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="oi oi-document me-2"></i> PDF Vorschau</h5>
|
||||
<button type="button" class="btn-close" @onclick="ClosePdfPreview"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0" style="flex: 1; overflow: hidden;">
|
||||
<iframe src="@_pdfPreviewUrl" style="width: 100%; height: 100%; border: none;"></iframe>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="ClosePdfPreview">Schließen</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="DownloadPdf">
|
||||
<i class="oi oi-data-transfer-download me-2"></i> Herunterladen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
@@ -214,6 +236,8 @@ else
|
||||
|
||||
private bool _showReportModal = false;
|
||||
private string _reportReason = "";
|
||||
private bool _showPdfModal = false;
|
||||
private string _pdfPreviewUrl = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -239,9 +263,22 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrintPage()
|
||||
private void ShowPdfPreview()
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("window.print");
|
||||
_pdfPreviewUrl = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}";
|
||||
_showPdfModal = true;
|
||||
}
|
||||
|
||||
private void ClosePdfPreview()
|
||||
{
|
||||
_showPdfModal = false;
|
||||
_pdfPreviewUrl = "";
|
||||
}
|
||||
|
||||
private async Task DownloadPdf()
|
||||
{
|
||||
var url = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}&download=true";
|
||||
await JSRuntime.InvokeVoidAsync("eval", $"var a = document.createElement('a'); a.href = '{url}'; a.download = 'HallOfFame.pdf'; document.body.appendChild(a); a.click(); document.body.removeChild(a);");
|
||||
}
|
||||
|
||||
private void ShowReportModal()
|
||||
|
||||
@@ -79,11 +79,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mt-4 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary me-2" @onclick="@(() => Save("Draft"))" disabled="@_uploading">Als Entwurf speichern</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="@(() => Save("Published"))" disabled="@_uploading">Veröffentlichen</button>
|
||||
<NavLink class="btn btn-link ms-2" href="@NavigateUrl()">Abbrechen</NavLink>
|
||||
</div>
|
||||
@if (PageState.Action == "Edit")
|
||||
{
|
||||
<button type="button" class="btn btn-outline-danger" @onclick="DeleteEntry" disabled="@_uploading">
|
||||
<i class="oi oi-trash me-1"></i> Eintrag löschen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<br /><br />
|
||||
@if (PageState.Action == "Edit")
|
||||
@@ -265,4 +273,18 @@
|
||||
AddModuleMessage(Localizer["Message.SaveError"], MessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteEntry()
|
||||
{
|
||||
try
|
||||
{
|
||||
await HallOfFameService.DeleteHallOfFameAsync(_id, ModuleState.ModuleId);
|
||||
NavigationManager.NavigateTo(NavigateUrl());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await logger.LogError(ex, "Error Deleting HallOfFame {Error}", ex.Message);
|
||||
AddModuleMessage("Fehler beim Löschen des Eintrags.", MessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,8 @@ else
|
||||
{
|
||||
try
|
||||
{
|
||||
_HallOfFames = await HallOfFameService.GetHallOfFamesAsync(ModuleState.ModuleId);
|
||||
var allEntries = await HallOfFameService.GetHallOfFamesAsync(ModuleState.ModuleId);
|
||||
_HallOfFames = allEntries.Where(i => i.Status == "Published").ToList();
|
||||
|
||||
if (PageState.User != null)
|
||||
{
|
||||
|
||||
310
QuestPDF_Integration.md
Normal file
310
QuestPDF_Integration.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# QuestPDF in Oqtane integrieren – Schritt für Schritt
|
||||
|
||||
## Überblick
|
||||
|
||||
Um PDF-Generierung in ein Oqtane-Modul zu integrieren, braucht man **5 Schritte**:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["1. NuGet installieren"] --> B["2. .csproj: DLL Copy Target"]
|
||||
B --> C["3. ServerStartup.cs: Lizenz"]
|
||||
C --> D["4. Controller erstellen"]
|
||||
D --> E["5. Razor-Seite: Vorschau"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schritt 1: QuestPDF installieren
|
||||
|
||||
Im Terminal, im **Server-Projekt**-Ordner:
|
||||
|
||||
```bash
|
||||
cd SZUAbsolventenverein.Module.HallOfFame/Server
|
||||
dotnet add package QuestPDF
|
||||
```
|
||||
|
||||
Das fügt automatisch folgende Zeile in die [Server.csproj](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/SZUAbsolventenverein.Module.HallOfFame.Server.csproj) ein:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="QuestPDF" Version="2026.2.1" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schritt 2: DLL-Copy Target in der .csproj
|
||||
|
||||
**Datei:** [Server.csproj](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/SZUAbsolventenverein.Module.HallOfFame.Server.csproj)
|
||||
|
||||
Oqtane lädt Module dynamisch – deshalb muss die QuestPDF.dll **und deren native SkiaSharp-Libraries** nach dem Build ins Oqtane-Server-Verzeichnis kopiert werden.
|
||||
|
||||
Dieses MSBuild-Target wird **am Ende der .csproj** vor `</Project>` eingefügt:
|
||||
|
||||
```xml
|
||||
<!-- Copy QuestPDF DLLs to Oqtane.Server so they are available at runtime -->
|
||||
<Target Name="CopyQuestPdfToOqtane" AfterTargets="Build">
|
||||
<!-- 1. Die Haupt-DLL kopieren -->
|
||||
<Copy SourceFiles="$(OutputPath)QuestPDF.dll"
|
||||
DestinationFolder="$(ProjectDir)..\..\oqtane.framework\Oqtane.Server\bin\$(Configuration)\$(TargetFramework)\"
|
||||
SkipUnchangedFiles="true" />
|
||||
<!-- 2. Die nativen SkiaSharp-Runtimes kopieren (für Mac, Windows, Linux) -->
|
||||
<ItemGroup>
|
||||
<QuestPdfNativeFiles Include="$(OutputPath)runtimes\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(QuestPdfNativeFiles)"
|
||||
DestinationFiles="@(QuestPdfNativeFiles->'$(ProjectDir)..\..\oqtane.framework\Oqtane.Server\bin\$(Configuration)\$(TargetFramework)\runtimes\%(RecursiveDir)%(Filename)%(Extension)')"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Ohne diesen Copy-Step bekommt man zur Laufzeit einen `FileNotFoundException` weil Oqtane die QuestPDF.dll nicht findet!
|
||||
|
||||
### Was wird wohin kopiert?
|
||||
|
||||
```
|
||||
Server/bin/Debug/net9.0/
|
||||
├── QuestPDF.dll ──→ oqtane.framework/Oqtane.Server/bin/Debug/net9.0/
|
||||
└── runtimes/
|
||||
├── osx/native/libSkiaSharp.dylib ──→ .../runtimes/osx/native/
|
||||
├── win-x64/native/libSkiaSharp.dll ──→ .../runtimes/win-x64/native/
|
||||
└── linux-x64/native/libSkiaSharp.so ──→ .../runtimes/linux-x64/native/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schritt 3: QuestPDF-Lizenz in ServerStartup.cs konfigurieren
|
||||
|
||||
**Datei:** [ServerStartup.cs](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/Startup/ServerStartup.cs) *(existierende Datei im Server/Startup Ordner)*
|
||||
|
||||
QuestPDF verlangt, dass man die Lizenz setzt, bevor man PDFs generiert. Das passiert in der `ConfigureServices` Methode:
|
||||
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// QuestPDF Lizenz konfigurieren (MUSS vor dem ersten PDF-Aufruf stehen!)
|
||||
QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community;
|
||||
|
||||
// ... andere Services
|
||||
services.AddTransient<IHallOfFameService, ServerHallOfFameService>();
|
||||
}
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> Ohne `QuestPDF.Settings.License = LicenseType.Community` bekommt man eine Exception beim Generieren des ersten PDFs!
|
||||
|
||||
---
|
||||
|
||||
## Schritt 4: PDF-Controller erstellen
|
||||
|
||||
**Datei:** [HallOfFamePDFController.cs](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/Controllers/HallOfFamePDFController.cs) *(neue Datei im Server/Controllers Ordner)*
|
||||
|
||||
### 2a) Usings und Klasse anlegen
|
||||
|
||||
```csharp
|
||||
using QuestPDF.Fluent; // Document.Create, .Text(), .Image() etc.
|
||||
using QuestPDF.Helpers; // PageSizes.A4, Colors
|
||||
using QuestPDF.Infrastructure; // IContainer, IDocument
|
||||
```
|
||||
|
||||
Der Controller erbt von `ModuleControllerBase` (Oqtane) und braucht:
|
||||
- `IHallOfFameService` – Einträge aus der DB laden
|
||||
- `IWebHostEnvironment` – Bildpfade auflösen (`WebRootPath`)
|
||||
|
||||
```csharp
|
||||
[Route(ControllerRoutes.ApiRoute)]
|
||||
public class HallOfFamePdfController : ModuleControllerBase
|
||||
{
|
||||
private readonly IHallOfFameService _hallOfFameService;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public HallOfFamePdfController(
|
||||
IHallOfFameService hallOfFameService,
|
||||
ILogManager logger,
|
||||
IHttpContextAccessor accessor,
|
||||
IWebHostEnvironment environment) : base(logger, accessor)
|
||||
{
|
||||
_hallOfFameService = hallOfFameService;
|
||||
_environment = environment;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2b) GET-Endpoint für PDF
|
||||
|
||||
```csharp
|
||||
// GET: api/HallOfFamePdf?moduleid=40&download=true|false
|
||||
[HttpGet]
|
||||
[Authorize(Policy = PolicyNames.ViewModule)]
|
||||
public async Task<IActionResult> Get(string moduleid, bool download = false)
|
||||
```
|
||||
|
||||
- `download=false` → PDF wird inline angezeigt (für iframe-Vorschau)
|
||||
- `download=true` → PDF wird als Datei heruntergeladen
|
||||
|
||||
### 2c) Bilder laden
|
||||
|
||||
Die Bilder liegen im `wwwroot` Ordner. Der Pfad wird mit `IWebHostEnvironment` aufgelöst:
|
||||
|
||||
```csharp
|
||||
byte[] imageBytes = null;
|
||||
if (!string.IsNullOrEmpty(entry.Image))
|
||||
{
|
||||
var fullImagePath = System.IO.Path.Combine(
|
||||
_environment.WebRootPath, entry.Image.TrimStart('/'));
|
||||
if (System.IO.File.Exists(fullImagePath))
|
||||
imageBytes = System.IO.File.ReadAllBytes(fullImagePath);
|
||||
}
|
||||
```
|
||||
|
||||
### 2d) PDF-Dokument mit Layers API aufbauen
|
||||
|
||||
So funktioniert das Hintergrundbild mit Text darüber:
|
||||
|
||||
```csharp
|
||||
var document = Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(0); // kein Rand = Bild füllt die ganze Seite
|
||||
|
||||
page.Content().Layers(layers =>
|
||||
{
|
||||
// 1. HINTERGRUND-Layer (VOR PrimaryLayer = wird dahinter gezeichnet)
|
||||
layers.Layer().Image(imageBytes).FitUnproportionally();
|
||||
|
||||
// 2. TEXT-Layer (PrimaryLayer = bestimmt die Seitengröße)
|
||||
layers.PrimaryLayer().Column(column =>
|
||||
{
|
||||
// Oben: Name-Box
|
||||
column.Item()
|
||||
.Background("#AA9E9E9E") // halbtransparentes Grau
|
||||
.Padding(20)
|
||||
.Text(entry.Name).FontSize(28).Bold().FontColor(Colors.White);
|
||||
|
||||
// Mitte: Freiraum (Bild sichtbar)
|
||||
// Unten: Beschreibung
|
||||
column.Item().ExtendVertical().AlignBottom()
|
||||
.Background("#CC333333") // dunkles Overlay
|
||||
.Padding(25)
|
||||
.Text(description).FontSize(11).FontColor(Colors.White);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// PDF als Byte-Array generieren
|
||||
byte[] pdfBytes = document.GeneratePdf();
|
||||
return File(pdfBytes, "application/pdf");
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> **Layers-Reihenfolge:** Layer VOR `PrimaryLayer` = Hintergrund. Layer NACH `PrimaryLayer` = Vordergrund/Wasserzeichen.
|
||||
|
||||
### 2e) Farben mit Transparenz
|
||||
|
||||
QuestPDF unterstützt Hex-Farben mit Alpha-Kanal (ARGB):
|
||||
|
||||
| Farbe | Code | Bedeutung |
|
||||
|---|---|---|
|
||||
| `#AA9E9E9E` | `AA` = 67% sichtbar, `9E9E9E` = Grau | Halbtransparentes Grau |
|
||||
| `#CC333333` | `CC` = 80% sichtbar, `333333` = Dunkelgrau | Dunkles Overlay |
|
||||
| `#FF000000` | Voll deckend Schwarz | Kein Durchschein |
|
||||
| `#00000000` | Komplett transparent | Unsichtbar |
|
||||
|
||||
---
|
||||
|
||||
## Schritt 5: Client-Seite (Blazor Razor)
|
||||
|
||||
**Datei:** [Details.razor](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Details.razor)
|
||||
|
||||
### 3a) Buttons für Vorschau und Download
|
||||
|
||||
```html
|
||||
<button class="btn btn-primary" @onclick="ShowPdfPreview">
|
||||
<i class="oi oi-eye me-2"></i> PDF Vorschau
|
||||
</button>
|
||||
```
|
||||
|
||||
### 3b) Modal mit iframe
|
||||
|
||||
Das PDF wird in einem `<iframe>` direkt im Browser angezeigt:
|
||||
|
||||
```html
|
||||
@if (_showPdfModal)
|
||||
{
|
||||
<div class="modal" style="display:block">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">PDF Vorschau</h5>
|
||||
<button class="btn-close" @onclick="ClosePdfPreview"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<iframe src="@_pdfPreviewUrl"
|
||||
style="width:100%; height:100%; border:none;">
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @onclick="ClosePdfPreview">Schließen</button>
|
||||
<button class="btn btn-primary" @onclick="DownloadPdf">
|
||||
<i class="oi oi-data-transfer-download me-2"></i> Herunterladen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
### 3c) C#-Methoden im @code Block
|
||||
|
||||
```csharp
|
||||
private bool _showPdfModal = false;
|
||||
private string _pdfPreviewUrl = "";
|
||||
|
||||
private void ShowPdfPreview()
|
||||
{
|
||||
// Vorschau: download=false → PDF wird inline gerendert
|
||||
_pdfPreviewUrl = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}";
|
||||
_showPdfModal = true;
|
||||
}
|
||||
|
||||
private void ClosePdfPreview()
|
||||
{
|
||||
_showPdfModal = false;
|
||||
_pdfPreviewUrl = "";
|
||||
}
|
||||
|
||||
private async Task DownloadPdf()
|
||||
{
|
||||
// Download: download=true → Browser lädt die Datei herunter
|
||||
var url = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}&download=true";
|
||||
await JSRuntime.InvokeVoidAsync("eval",
|
||||
$"var a = document.createElement('a'); a.href = '{url}'; " +
|
||||
$"a.download = 'HallOfFame.pdf'; document.body.appendChild(a); " +
|
||||
$"a.click(); document.body.removeChild(a);");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bauen und Testen
|
||||
|
||||
```bash
|
||||
# 1. Modul bauen (kopiert automatisch die QuestPDF DLLs)
|
||||
dotnet build SZUAbsolventenverein.Module.HallOfFame.sln
|
||||
|
||||
# 2. Oqtane Server starten
|
||||
cd oqtane.framework
|
||||
dotnet run --project Oqtane.Server/Oqtane.Server.csproj
|
||||
|
||||
# 3. Im Browser testen
|
||||
# http://localhost:44357 → Hall of Fame → Details ansehen → PDF Vorschau
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ergebnis
|
||||
|
||||

|
||||
144
Server/Controllers/HallOfFamePDFController.cs
Normal file
144
Server/Controllers/HallOfFamePDFController.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Oqtane.Shared;
|
||||
using Oqtane.Enums;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Controllers;
|
||||
using SZUAbsolventenverein.Module.HallOfFame.Services;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
|
||||
{
|
||||
[Route(ControllerRoutes.ApiRoute)]
|
||||
public class HallOfFamePdfController : ModuleControllerBase
|
||||
{
|
||||
private readonly IHallOfFameService _hallOfFameService;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public HallOfFamePdfController(IHallOfFameService hallOfFameService, ILogManager logger, IHttpContextAccessor accessor, IWebHostEnvironment environment) : base(logger, accessor)
|
||||
{
|
||||
_hallOfFameService = hallOfFameService;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
// GET: api/<controller>?moduleid=x&download=true/false
|
||||
[HttpGet]
|
||||
[Authorize(Policy = PolicyNames.ViewModule)]
|
||||
public async Task<IActionResult> Get(string moduleid, bool download = false)
|
||||
{
|
||||
int ModuleId;
|
||||
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
|
||||
{
|
||||
var entries = await _hallOfFameService.GetHallOfFamesAsync(ModuleId);
|
||||
var publishedEntries = entries.Where(e => e.Status == "Published").ToList();
|
||||
|
||||
var document = Document.Create(container =>
|
||||
{
|
||||
foreach (var entry in publishedEntries)
|
||||
{
|
||||
// Bild laden falls vorhanden
|
||||
byte[] imageBytes = null;
|
||||
if (!string.IsNullOrEmpty(entry.Image))
|
||||
{
|
||||
try
|
||||
{
|
||||
var fullImagePath = System.IO.Path.Combine(
|
||||
_environment.WebRootPath, entry.Image.TrimStart('/'));
|
||||
if (System.IO.File.Exists(fullImagePath))
|
||||
imageBytes = System.IO.File.ReadAllBytes(fullImagePath);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(0);
|
||||
|
||||
page.Content().Layers(layers =>
|
||||
{
|
||||
// Hintergrund-Layer: Bild als Vollbild
|
||||
if (imageBytes != null)
|
||||
{
|
||||
layers.Layer().Image(imageBytes).FitUnproportionally();
|
||||
}
|
||||
else
|
||||
{
|
||||
layers.Layer().Background(Colors.Grey.Darken3);
|
||||
}
|
||||
|
||||
// Inhalt-Layer (PrimaryLayer bestimmt die Größe)
|
||||
layers.PrimaryLayer()
|
||||
.Column(column =>
|
||||
{
|
||||
// === OBEN: Name und Jahr in halbtransparenter grauer Box ===
|
||||
column.Item()
|
||||
.Background("#AA9E9E9E") // halbtransparentes Grau
|
||||
.Padding(20)
|
||||
.Row(row =>
|
||||
{
|
||||
row.RelativeItem().Column(inner =>
|
||||
{
|
||||
inner.Item().Text(entry.Name)
|
||||
.FontSize(28).Bold().FontColor(Colors.White);
|
||||
inner.Item().Text($"Jahrgang {entry.Year}")
|
||||
.FontSize(14).FontColor(Colors.White);
|
||||
});
|
||||
});
|
||||
|
||||
// === UNTEN: Beschreibung am Seitenende ===
|
||||
var description = entry.Description ?? "";
|
||||
var sections = description.Split('\n')
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToArray();
|
||||
|
||||
column.Item().ExtendVertical().AlignBottom()
|
||||
.Background("#CC333333") // dunkles halbtransparentes Overlay
|
||||
.Padding(25)
|
||||
.Column(descColumn =>
|
||||
{
|
||||
if (sections.Length > 0)
|
||||
{
|
||||
descColumn.Item().PaddingBottom(10)
|
||||
.Text("Beschreibung:")
|
||||
.FontSize(16).Bold().FontColor(Colors.White);
|
||||
|
||||
foreach (var line in sections)
|
||||
{
|
||||
descColumn.Item().PaddingBottom(6)
|
||||
.Text(line)
|
||||
.FontSize(11).FontColor(Colors.White)
|
||||
.LineHeight(1.4f);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
byte[] pdfBytes = document.GeneratePdf();
|
||||
|
||||
if (download)
|
||||
{
|
||||
return File(pdfBytes, "application/pdf", "HallOfFame.pdf");
|
||||
}
|
||||
// Inline: PDF wird im Browser angezeigt (Vorschau)
|
||||
return File(pdfBytes, "application/pdf");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame PDF Get Attempt {ModuleId}", moduleid);
|
||||
return Forbid();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.8" />
|
||||
<PackageReference Include="QuestPDF" Version="2026.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -34,4 +35,13 @@
|
||||
<Reference Include="Oqtane.Server"><HintPath>..\..\oqtane.framework\Oqtane.Server\bin\Debug\net9.0\Oqtane.Server.dll</HintPath></Reference>
|
||||
<Reference Include="Oqtane.Shared"><HintPath>..\..\oqtane.framework\Oqtane.Server\bin\Debug\net9.0\Oqtane.Shared.dll</HintPath></Reference>
|
||||
</ItemGroup>
|
||||
<!-- Copy QuestPDF DLLs to Oqtane.Server so they are available at runtime -->
|
||||
<Target Name="CopyQuestPdfToOqtane" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(OutputPath)QuestPDF.dll" DestinationFolder="$(ProjectDir)..\..\oqtane.framework\Oqtane.Server\bin\$(Configuration)\$(TargetFramework)\" SkipUnchangedFiles="true" />
|
||||
<ItemGroup>
|
||||
<QuestPdfNativeFiles Include="$(OutputPath)runtimes\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(QuestPdfNativeFiles)" DestinationFiles="@(QuestPdfNativeFiles->'$(ProjectDir)..\..\oqtane.framework\Oqtane.Server\bin\$(Configuration)\$(TargetFramework)\runtimes\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -21,6 +21,9 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Startup
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// QuestPDF Lizenz konfigurieren
|
||||
QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community;
|
||||
|
||||
services.AddTransient<IHallOfFameService, ServerHallOfFameService>();
|
||||
services.AddDbContextFactory<HallOfFameContext>(opt => { }, ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user