Files
EnvelopeGenerator/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor
TekH f1ebd47c77 Refactor EnvelopeViewer styles and add PDF support
Moved inline styles from `EnvelopeViewer.razor` to a new external
CSS file `envelope-viewer.css` for better maintainability and
separation of concerns. Added external JavaScript dependencies
(`pdf.js` and `pdf-viewer.js`) to enable PDF rendering.

Fixed encoding issues in error messages and improved the layout
structure with responsive design rules. Enhanced readability and
usability by organizing styles and ensuring proper localization
support.
2026-06-05 11:18:23 +02:00

193 lines
9.2 KiB
Plaintext
Raw Blame History

@page "/envelope/{EnvelopeKey}"
@using EnvelopeGenerator.ReceiverUI.Services
@using Microsoft.Extensions.Options
@using EnvelopeGenerator.ReceiverUI.Options
@using Microsoft.JSInterop
@inject DocumentService DocumentService
@inject NavigationManager Navigation
@inject IOptions<ApiOptions> AppOptions
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="css/envelope-viewer.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="js/pdf-viewer.js"></script>
<div class="envelope-viewer-layout">
<div class="envelope-action-bar">
<div class="envelope-action-bar__inner">
<div class="d-flex align-items-center gap-3">
<div class="envelope-logo">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
</svg>
</div>
<div>
<div class="envelope-title">Dokumentenansicht</div>
<div class="envelope-key">ID: @EnvelopeKey</div>
</div>
</div>
@if (_pdfLoaded) {
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="pdf-controls">
<button class="btn btn-sm btn-outline-primary" @onclick="ZoomOut" title="Zoom Out">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M6.5 1A5.5 5.5 0 0 0 1 6.5v3A5.5 5.5 0 0 0 6.5 15h3a5.5 5.5 0 0 0 5.5-5.5v-3A5.5 5.5 0 0 0 9.5 1h-3zM4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
</svg>
</button>
<span class="zoom-level">@(_currentZoom)%</span>
<button class="btn btn-sm btn-outline-primary" @onclick="ZoomIn" title="Zoom In">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M6.5 1A5.5 5.5 0 0 0 1 6.5v3A5.5 5.5 0 0 0 6.5 15h3a5.5 5.5 0 0 0 5.5-5.5v-3A5.5 5.5 0 0 0 9.5 1h-3zM8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</button>
</div>
<div class="pdf-navigation">
<button class="btn btn-sm btn-primary" @onclick="PreviousPage" disabled="@(_currentPage <= 1)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
</button>
<span class="page-info">Seite @_currentPage / @_totalPages</span>
<button class="btn btn-sm btn-primary" @onclick="NextPage" disabled="@(_currentPage >= _totalPages)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
</div>
}
</div>
</div>
<div class="envelope-content">
@if (_isLoading) {
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center">
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
<span class="visually-hidden">L<>dt...</span>
</div>
<p class="text-white fw-semibold">Dokument wird geladen...</p>
</div>
</div>
} else if (_errorMessage is not null) {
<div class="error-container">
<div class="alert alert-danger shadow-lg">
<div class="d-flex align-items-start">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<div>
<h5 class="mb-2">Fehler beim Laden des Dokuments</h5>
<p class="mb-0">@_errorMessage</p>
</div>
</div>
</div>
</div>
} else if (!string.IsNullOrWhiteSpace(_pdfDataUrl)) {
<div class="pdf-viewer-container">
<div class="pdf-frame">
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
</div>
</div>
} else {
<div class="error-container">
<div class="alert alert-warning shadow-lg">
<div class="d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
<span class="fs-5">Dokument konnte nicht geladen werden.</span>
</div>
</div>
</div>
}
</div>
</div>
@code {
[Parameter] public string? EnvelopeKey { get; set; }
bool _isLoading = true;
string? _errorMessage;
string? _pdfDataUrl;
bool _pdfLoaded = false;
int _currentPage = 1;
int _totalPages = 0;
int _currentZoom = 150;
protected override async Task OnInitializedAsync() {
if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
_errorMessage = "Envelope-Schlüssel fehlt.";
_isLoading = false;
return;
}
try {
var (pdfBytes, statusCode) = await DocumentService.GetDocumentAsync(EnvelopeKey);
if (pdfBytes is { Length: > 0 }) {
var base64 = Convert.ToBase64String(pdfBytes);
_pdfDataUrl = $"data:application/pdf;base64,{base64}";
} else {
_errorMessage = $"Dokument konnte nicht geladen werden. HTTP Status: {statusCode}";
}
} catch (Exception ex) {
_errorMessage = $"Fehler: {ex.Message}";
}
_isLoading = false;
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (!_pdfLoaded && !string.IsNullOrWhiteSpace(_pdfDataUrl)) {
await Task.Delay(500);
try {
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl);
if (success) {
_pdfLoaded = true;
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
await InvokeAsync(StateHasChanged);
}
} catch (Exception ex) {
_errorMessage = $"PDF.js Fehler: {ex.Message}";
await InvokeAsync(StateHasChanged);
}
}
}
async Task NextPage() {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage")) {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
}
}
async Task PreviousPage() {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage")) {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
}
}
async Task ZoomIn() {
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
}
async Task ZoomOut() {
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
}
public async ValueTask DisposeAsync() {
// Cleanup if needed
}
}