A szállítólevél-grid fájlneve megnyitja a csatolt PDF-et vagy képet
- GridShippingDocument: a PdfFileName oszlop link, ha a soron van csatolmány; kattintásra a fájl a beépített megjelenítőben nyílik, DxWindow-ban. - pdfViewer.renderBytes: memóriából érkező fájl rajzolása — PDF-nél pdf.js, képnél objektum-URL. A renderPdfs URL-es útja változatlan. - A tartalom szerződése a dróton: SignalRTags 69, IFruitBankDataControllerCommon és a kliens wrappere.
This commit is contained in:
@@ -151,6 +151,11 @@ public interface IFruitBankDataControllerCommon
|
||||
public Task<List<ShippingDocument>?> GetShippingDocumentsByPartnerId(int partnerId);
|
||||
public Task<ShippingDocument?> AddShippingDocument(ShippingDocument shippingDocument);
|
||||
public Task<ShippingDocument?> UpdateShippingDocument(ShippingDocument shippingDocument);
|
||||
|
||||
/// <summary>A csatolt fájl nyers tartalma <c>Files.Id</c> alapján; null, ha a blob nem található.</summary>
|
||||
/// <remarks>A hívó a kiterjesztést a soron már megkapott <c>Files</c> rekordból tudja — a bájtok mellé
|
||||
/// nem megy külön típusinformáció.</remarks>
|
||||
public Task<byte[]?> GetShippingDocumentFileContent(int filesId);
|
||||
#endregion ShippingDocument
|
||||
|
||||
#region Customer
|
||||
|
||||
@@ -53,6 +53,7 @@ public class SignalRTags : AcSignalRTags
|
||||
public const int UpdateShippingDocument = 66;
|
||||
public const int CanDeleteShippingDocument = 67;
|
||||
public const int DeleteShippingDocument = 68;
|
||||
public const int GetShippingDocumentFileContent = 69;
|
||||
|
||||
public const int GetMeasuringUsers = 70;
|
||||
public const int GetCustomerDtoById = 71;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
@inject IEnumerable<IAcLogWriterClientBase> LogWriters
|
||||
@inject FruitBankSignalRClient FruitBankSignalRClient
|
||||
@inject GuardedDeleteService GuardedDelete
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<MgGridWithInfoPanel ShowInfoPanel="@IsMasterGrid">
|
||||
<GridContent>
|
||||
@@ -96,7 +97,28 @@
|
||||
<DxGridDataColumn FieldName="TotalPallets" />
|
||||
<DxGridDataColumn FieldName="IsAllMeasured" ReadOnly="true" />
|
||||
<DxGridDataColumn FieldName="Comment" />
|
||||
<DxGridDataColumn FieldName="PdfFileName" />
|
||||
@* A cella SZÖVEGE a dokumentumé (PdfFileName), a link CÉLJA viszont a csatolt Files rekord:
|
||||
hash-egyezéskor a fájl egy korábbi feltöltésé, más néven — a tényleges tárolt név a tooltipben.
|
||||
Csatolmány nélküli soron marad a sima szöveg. *@
|
||||
<DxGridDataColumn FieldName="PdfFileName">
|
||||
<CellDisplayTemplate>
|
||||
@{
|
||||
var fileDocument = (ShippingDocument)context.DataItem;
|
||||
var attachedFile = AttachedFileOf(fileDocument);
|
||||
}
|
||||
@if (attachedFile == null)
|
||||
{
|
||||
@context.DisplayText
|
||||
}
|
||||
else
|
||||
{
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Link" CssClass="p-0"
|
||||
Text="@context.DisplayText"
|
||||
Title="@($"Megnyitás: {attachedFile.FileName}{attachedFile.FileExtension}")"
|
||||
Click="() => OpenAttachedFileAsync(fileDocument, attachedFile)" />
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Created" ReadOnly="true" />
|
||||
<DxGridDataColumn FieldName="Modified" ReadOnly="true" />
|
||||
@* DeleteButtonVisible=false: a beépített törlés a keret RemoveMessageTag-jére mentene, ami
|
||||
@@ -200,6 +222,30 @@
|
||||
<AiProcessFormTemplate />
|
||||
</BodyContentTemplate>
|
||||
</DxWindow>
|
||||
|
||||
@* A csatolt szállítólevél megjelenítője. A fájl a SignalR-en jön (nincs URL, amit a böngésző letölthetne),
|
||||
ezért ugyanez a kód fut a web hostban és a MAUI WebView-ban is. *@
|
||||
<DxWindow AllowResize="true"
|
||||
ShowCloseButton="true"
|
||||
CloseOnEscape="true"
|
||||
ShowHeader="true"
|
||||
HeaderText="@_pdfWindowTitle"
|
||||
ShowFooter="false"
|
||||
SizeMode="SizeMode.Large"
|
||||
Width="90vw"
|
||||
Height="90vh"
|
||||
@bind-Visible="_pdfWindowVisible">
|
||||
<BodyContentTemplate Context="ctxPdfBody">
|
||||
@if (_pdfError != null)
|
||||
{
|
||||
<div class="text-danger p-3">@_pdfError</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="@_pdfContainerId" style="width: 100%; height: 100%; overflow-y: auto;"></div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
</DxWindow>
|
||||
@code {
|
||||
[Inject] public required DatabaseClient Database { get; set; }
|
||||
[Inject] public required LoggedInModel LoggedInModel { get; set; }
|
||||
@@ -240,6 +286,73 @@
|
||||
}
|
||||
}
|
||||
|
||||
private bool _pdfWindowVisible;
|
||||
private string _pdfWindowTitle = string.Empty;
|
||||
private string? _pdfError;
|
||||
|
||||
// Példányonkénti konténer-azonosító: ez a rács master-ként és detail-ként is a lapon lehet, és a
|
||||
// megjelenítő getElementById-vel keresi a helyét — két azonos id közül a másikat találná meg.
|
||||
private readonly string _pdfContainerId = $"shippingDocumentPdf_{Guid.NewGuid():N}";
|
||||
|
||||
/// <summary>A dokumentumhoz csatolt fájl rekordja, vagy null, ha nincs csatolmány.</summary>
|
||||
/// <remarks>Ma egy csatolmány van dokumentumonként, ezért a legrégebbi a link célja. A több csatolmányt
|
||||
/// külön fájlkezelő felület fogja kezelni.</remarks>
|
||||
private static Files? AttachedFileOf(ShippingDocument? shippingDocument)
|
||||
=> shippingDocument?.ShippingDocumentToFiles?
|
||||
.OrderBy(mapping => mapping.Id)
|
||||
.Select(mapping => mapping.ShippingDocumentFile)
|
||||
.FirstOrDefault(file => file != null);
|
||||
|
||||
/// <summary>
|
||||
/// A csatolt szállítólevél megnyitása a beépített megjelenítőben.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A sorrend szándékos: előbb az ablak (a rajzoló csak létező konténerbe tud dolgozni), utána a letöltés.
|
||||
/// A letöltés await-je adja azt a render-kört, amiben a konténer megjelenik; a <c>renderBytes</c> ettől
|
||||
/// függetlenül is megvárja.
|
||||
/// </remarks>
|
||||
private async Task OpenAttachedFileAsync(ShippingDocument shippingDocument, Files file)
|
||||
{
|
||||
_pdfError = null;
|
||||
_pdfWindowTitle = $"{shippingDocument.DocumentIdNumber} — {file.FileName}{file.FileExtension}";
|
||||
_pdfWindowVisible = true;
|
||||
|
||||
try
|
||||
{
|
||||
var content = await FruitBankSignalRClient.GetShippingDocumentFileContent(file.Id);
|
||||
|
||||
if (content == null || content.Length == 0)
|
||||
{
|
||||
_pdfError = "A csatolt fájl nem található a tárolóban.";
|
||||
_logger.Warning($"ShippingDocument {shippingDocument.Id}: a(z) {file.Id} Files rekord blobja nem található.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Az ablak bezárásakor a konténer eltűnik a DOM-ból, a megjelenítő viszont a kulcs alapján
|
||||
// kihagyná az újrarajzolást — ezért minden megnyitás előtt ejtjük a rajzolás-cache-t. A kibontott
|
||||
// dokumentum a saját cache-ében marad, tehát az újranyitás így is olcsó.
|
||||
await JS.InvokeVoidAsync("pdfViewer.clearRenderCache");
|
||||
await JS.InvokeVoidAsync("pdfViewer.renderBytes", _pdfContainerId, $"files-{file.Id}", content, MimeTypeOf(file.FileExtension));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_pdfError = "A csatolt fájl megnyitása nem sikerült.";
|
||||
_logger.Error($"ShippingDocument {shippingDocument.Id}: a(z) {file.Id} Files rekord megnyitása elszállt.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A megjelenítő ebből dönti el, hogy PDF-et rajzol vagy képet mutat.</summary>
|
||||
private static string MimeTypeOf(string? fileExtension) => (fileExtension ?? string.Empty).ToLowerInvariant() switch
|
||||
{
|
||||
".pdf" => "application/pdf",
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".webp" => "image/webp",
|
||||
".bmp" => "image/bmp",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
|
||||
public bool IsMasterGrid => ParentDataItem == null;
|
||||
public bool ParentDataItemIsShipping => (ParentDataItem is Shipping);
|
||||
public bool ParentDataItemIsPartner => (ParentDataItem is Partner);
|
||||
|
||||
@@ -372,6 +372,9 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
||||
|
||||
public Task<ShippingDocument?> UpdateShippingDocument(ShippingDocument shippingDocument)
|
||||
=> PostDataAsync(SignalRTags.UpdateShippingDocument, shippingDocument);
|
||||
|
||||
public Task<byte[]?> GetShippingDocumentFileContent(int filesId)
|
||||
=> GetByIdAsync<byte[]?>(SignalRTags.GetShippingDocumentFileContent, filesId);
|
||||
#endregion ShippingDocument
|
||||
|
||||
#region Customer
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
window.pdfViewer = {
|
||||
_pdfCache: new Map(),
|
||||
_objectUrlCache: new Map(),
|
||||
_resizeObserver: null,
|
||||
_currentContainerId: null,
|
||||
_currentPdfUrls: null,
|
||||
_currentSources: null,
|
||||
_renderTimeout: null,
|
||||
_lastRenderedUrls: null, // Track what was last rendered
|
||||
_lastRenderedKey: null, // Track what was last rendered
|
||||
|
||||
// A böngésző tölti le a PDF-eket a megadott URL-ekről.
|
||||
renderPdfs: async function (containerId, pdfUrls) {
|
||||
const sources = (pdfUrls || []).map(url => ({ key: url, src: url, kind: 'pdf' }));
|
||||
await this._render(containerId, sources);
|
||||
},
|
||||
|
||||
// Egyetlen fájl a memóriából: a bájtok a SignalR-en jönnek, nincs URL, amit a böngésző letölthetne.
|
||||
// A cacheKey azonosítja a fájlt (pl. "files-123") — bájtokra magukra nem lehet cache-kulcsot képezni.
|
||||
renderBytes: async function (containerId, cacheKey, bytes, mimeType) {
|
||||
const kind = (mimeType || '').startsWith('image/') ? 'image' : 'pdf';
|
||||
await this._render(containerId, [{ key: cacheKey, src: bytes, kind: kind, mimeType: mimeType }]);
|
||||
},
|
||||
|
||||
_render: async function (containerId, sources) {
|
||||
// Wait for container to be available
|
||||
let container = null;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
@@ -20,25 +34,26 @@ window.pdfViewer = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if URLs changed - if same, skip render (use cache)
|
||||
const urlsKey = JSON.stringify(pdfUrls);
|
||||
if (this._lastRenderedUrls === urlsKey && this._currentContainerId === containerId) {
|
||||
console.log('[PDF] Same URLs, skipping render (cached)');
|
||||
// Check if the source set changed - if same, skip render (use cache).
|
||||
// A kulcsokra képezzük, nem a forrásokra: a bájttömb JSON-ba írása megölné a lapot.
|
||||
const sourcesKey = JSON.stringify(sources.map(s => s.key));
|
||||
if (this._lastRenderedKey === sourcesKey && this._currentContainerId === containerId) {
|
||||
console.log('[PDF] Same sources, skipping render (cached)');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PDF] New URLs detected, rendering:', pdfUrls);
|
||||
console.log('[PDF] New sources detected, rendering:', sources.map(s => s.key));
|
||||
|
||||
// Store for resize handling
|
||||
this._currentContainerId = containerId;
|
||||
this._currentPdfUrls = pdfUrls;
|
||||
this._lastRenderedUrls = urlsKey;
|
||||
this._currentSources = sources;
|
||||
this._lastRenderedKey = sourcesKey;
|
||||
|
||||
// Setup resize observer
|
||||
this._setupResizeObserver(container);
|
||||
|
||||
// Render
|
||||
await this._doRender(container, pdfUrls);
|
||||
await this._doRender(container, sources);
|
||||
},
|
||||
|
||||
_setupResizeObserver: function(container) {
|
||||
@@ -59,7 +74,7 @@ window.pdfViewer = {
|
||||
clearTimeout(this._renderTimeout);
|
||||
}
|
||||
this._renderTimeout = setTimeout(() => {
|
||||
this._doRender(container, this._currentPdfUrls);
|
||||
this._doRender(container, this._currentSources);
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
@@ -67,10 +82,11 @@ window.pdfViewer = {
|
||||
this._resizeObserver.observe(container);
|
||||
},
|
||||
|
||||
_doRender: async function(container, pdfUrls) {
|
||||
_doRender: async function(container, sources) {
|
||||
container.innerHTML = '';
|
||||
|
||||
if (typeof pdfjsLib === 'undefined') {
|
||||
// A kép-ág nem használja a pdf.js-t, ezért csak akkor bukunk el rajta, ha tényleg PDF-et kérnek.
|
||||
if (typeof pdfjsLib === 'undefined' && sources.some(s => s.kind !== 'image')) {
|
||||
console.error('PDF.js not loaded');
|
||||
container.innerHTML = '<p style="color:red;">PDF.js nincs betöltve</p>';
|
||||
return;
|
||||
@@ -85,19 +101,33 @@ window.pdfViewer = {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PDF] Rendering at width:', containerWidth, 'URLs:', pdfUrls);
|
||||
console.log('[PDF] Rendering at width:', containerWidth, 'sources:', sources.map(s => s.key));
|
||||
|
||||
for (const url of pdfUrls) {
|
||||
for (const source of sources) {
|
||||
const url = source.key;
|
||||
try {
|
||||
if (source.kind === 'image') {
|
||||
const img = document.createElement('img');
|
||||
img.src = this._toObjectUrl(source);
|
||||
img.style.width = '100%';
|
||||
img.style.display = 'block';
|
||||
img.style.marginBottom = '8px';
|
||||
container.appendChild(img);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use cached PDF document if available (PDF.js document cache)
|
||||
let pdf = this._pdfCache.get(url);
|
||||
let pdf = this._pdfCache.get(source.key);
|
||||
if (!pdf) {
|
||||
console.log('[PDF] Loading new PDF:', url);
|
||||
const loadingTask = pdfjsLib.getDocument(url);
|
||||
console.log('[PDF] Loading new PDF:', source.key);
|
||||
// A bájttömböt MÁSOLVA adjuk át: a pdf.js a workernek átadva leválasztja (detach)
|
||||
// az ArrayBuffert, és a hívó példánya használhatatlanná válna egy újrarajzoláshoz.
|
||||
const loadingTask = pdfjsLib.getDocument(
|
||||
typeof source.src === 'string' ? source.src : { data: new Uint8Array(source.src) });
|
||||
pdf = await loadingTask.promise;
|
||||
this._pdfCache.set(url, pdf);
|
||||
this._pdfCache.set(source.key, pdf);
|
||||
} else {
|
||||
console.log('[PDF] Using cached PDF:', url);
|
||||
console.log('[PDF] Using cached PDF:', source.key);
|
||||
}
|
||||
|
||||
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
||||
@@ -134,6 +164,16 @@ window.pdfViewer = {
|
||||
}
|
||||
},
|
||||
|
||||
// Kulcsonként EGY objektum-URL: az átméretezés újrarajzol, és minden körben új URL-t gyártani szivárgás.
|
||||
_toObjectUrl: function(source) {
|
||||
let objectUrl = this._objectUrlCache.get(source.key);
|
||||
if (!objectUrl) {
|
||||
objectUrl = URL.createObjectURL(new Blob([source.src], { type: source.mimeType || 'application/octet-stream' }));
|
||||
this._objectUrlCache.set(source.key, objectUrl);
|
||||
}
|
||||
return objectUrl;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
@@ -144,11 +184,13 @@ window.pdfViewer = {
|
||||
}
|
||||
// Keep PDF cache for performance, only clear on full dispose
|
||||
this._pdfCache.clear();
|
||||
this._lastRenderedUrls = null;
|
||||
this._objectUrlCache.forEach(url => URL.revokeObjectURL(url));
|
||||
this._objectUrlCache.clear();
|
||||
this._lastRenderedKey = null;
|
||||
},
|
||||
|
||||
// Clear only the render cache (not PDF documents)
|
||||
clearRenderCache: function() {
|
||||
this._lastRenderedUrls = null;
|
||||
this._lastRenderedKey = null;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user