feat: add image and thumbnail upload support in DiscountProduct forms and services
Build and Deploy / build (push) Failing after 17s

This commit is contained in:
masoodafar-web
2026-01-05 01:23:49 +03:30
parent a2c496c16f
commit 53f56636b3
6 changed files with 277 additions and 142 deletions
Binary file not shown.
Binary file not shown.
@@ -1,121 +1,155 @@
@using BackOffice.Services.DiscountProduct
@using BackOffice.Services.DiscountCategory
@using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@inject ISnackbar Snackbar
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="@_isValid">
<MudGrid>
<MudItem xs="12">
<MudTextField @bind-Value="Model.Title"
<MudStack Spacing="3">
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
@if (!string.IsNullOrWhiteSpace(_mainImagePreview))
{
<Image Src="@(_mainImagePreview)" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else if (!string.IsNullOrEmpty(Model.ImagePath))
{
<Image Src="@Model.ImagePath" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else
{
<MudPaper Class="d-flex align-center justify-center" Style="width:220px;height:140px;">
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
</MudPaper>
}
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnMainImageSelected">
<ActivatorContent>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
ButtonType="ButtonType.Button"
StartIcon="@Icons.Material.Filled.Image"
Style="cursor:pointer;">
انتخاب تصویر اصلی
</MudButton>
</ActivatorContent>
<SelectedTemplate>
@if (context != null)
{
<MudText Class="mt-2" Typo="Typo.subtitle2">
<MudTooltip Text="@context.Name" Inline="true">
@context.Name
</MudTooltip>
</MudText>
}
else
{
<MudText Class="mt-2" Typo="Typo.subtitle2">فایلی انتخاب نشده</MudText>
}
</SelectedTemplate>
</MudFileUpload>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudTextField T="string" @bind-Value="Model.Title"
Disabled="_loading"
Label="عنوان محصول *"
Required="true"
RequiredError="عنوان الزامی است"
Variant="Variant.Outlined" />
Variant="Variant.Outlined"
Margin="Margin.Dense" />
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="Model.ShortInformation"
Label="توضیح کوتاه"
Lines="2"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="Model.FullInformation"
Label="توضیحات کامل"
Lines="4"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField @bind-Value="Model.Price"
<MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true"
@bind-Value="Model.Price"
Format="N0"
Immediate="true"
Disabled="_loading"
Label="قیمت (ریال) *"
Required="true"
Min="0"
Format="N0"
Variant="Variant.Outlined" />
Variant="Variant.Outlined"
Margin="Margin.Dense" />
</MudItem>
</MudStack>
<MudItem xs="12" sm="6">
<MudNumericField @bind-Value="Model.MaxDiscountPercent"
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true"
@bind-Value="Model.MaxDiscountPercent"
Disabled="_loading"
Label="حداکثر درصد تخفیف *"
Required="true"
Min="0"
Max="100"
Variant="Variant.Outlined" />
Variant="Variant.Outlined"
Margin="Margin.Dense" />
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField @bind-Value="Model.SortOrder"
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true"
@bind-Value="Model.SortOrder"
Disabled="_loading"
Label="ترتیب نمایش"
Min="0"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudSelect @bind-SelectedValues="_selectedCategoryIds"
Label="دسته‌بندی‌ها"
Variant="Variant.Outlined"
MultiSelection="true"
T="long">
@foreach (var category in _categories)
Margin="Margin.Dense" />
</MudItem>
</MudStack>
<MudTextField T="string" @bind-Value="Model.ShortInformation"
Disabled="_loading"
Label="توضیح کوتاه"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudHtmlEditor @bind-Html="Model.FullInformation">
<MudHtmlToolbarOptions InsertImage="false" />
</MudHtmlEditor>
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">دسته‌بندی‌ها</MudText>
<CategoryMultiSelectCombo @bind-SelectedIds="_selectedCategoryIds"
Label="انتخاب دسته‌بندی‌ها"
Disabled="_loading" />
@if (_selectedCategoryIds?.Count > 0)
{
<MudSelectItem Value="@category.Id">@GetCategoryPath(category)</MudSelectItem>
<MudText Typo="Typo.caption">
تعداد دسته‌بندی‌های انتخاب‌شده: @_selectedCategoryIds.Count
</MudText>
}
</MudSelect>
</MudItem>
</MudStack>
<MudItem xs="12" sm="6">
<MudTextField @bind-Value="Model.ImagePath"
Label="مسیر تصویر اصلی"
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">تصویر بندانگشتی</MudText>
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnThumbnailSelected">
<ActivatorContent>
<MudButton HtmlTag="label"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Image" />
</MudItem>
Color="Color.Secondary"
ButtonType="ButtonType.Button"
StartIcon="@Icons.Material.Filled.Image"
Style="cursor:pointer;">
انتخاب تصویر بندانگشتی (اختیاری)
</MudButton>
</ActivatorContent>
</MudFileUpload>
</MudStack>
<MudItem xs="12" sm="6">
<MudTextField @bind-Value="Model.ThumbnailPath"
Label="مسیر تصویر بندانگشتی"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Image" />
</MudItem>
@if (!string.IsNullOrEmpty(Model.ThumbnailPath))
{
<MudItem xs="12">
<MudImage Src="@Model.ThumbnailPath"
Alt="پیش‌نمایش"
Height="150"
ObjectFit="ObjectFit.Contain"
Class="rounded" />
</MudItem>
}
<MudItem xs="12">
<MudSwitch T="bool" @bind-Value="Model.IsActive"
Label="محصول فعال"
Color="Color.Success" />
</MudItem>
</MudGrid>
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Variant="Variant.Text">انصراف</MudButton>
<MudButton OnClick="Cancel" Disabled="_loading">انصراف</MudButton>
<MudButton OnClick="Submit"
Variant="Variant.Filled"
Color="Color.Primary"
Disabled="@(!_isValid || _loading)">
@if (_loading)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
}
else
{
<text>@(IsEditMode ? "ذخیره تغییرات" : "ایجاد محصول")</text>
}
</MudButton>
Disabled="@(!_isValid || _loading)">ثبت</MudButton>
</DialogActions>
</MudDialog>
@@ -128,56 +162,35 @@
private MudForm? _form;
private bool _isValid;
private bool _loading;
private List<DiscountCategoryDto> _categories = new();
private IEnumerable<long> _selectedCategoryIds = new List<long>();
private IBrowserFile? _mainImageFile;
private IBrowserFile? _thumbnailImageFile;
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
private string? _mainImagePreview;
private List<long> _selectedCategoryIds = new();
protected override async Task OnInitializedAsync()
{
await LoadCategories();
if (Model.CategoryIds?.Any() == true)
{
_selectedCategoryIds = Model.CategoryIds;
}
}
private async Task LoadCategories()
private async Task OnMainImageSelected(IBrowserFile? file)
{
try
_mainImageFile = file;
if (file != null)
{
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
_categories = FlattenCategories(tree);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری دسته‌بندی‌ها: {ex.Message}", Severity.Error);
var buffer = new byte[file.Size];
await file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
_mainImagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(buffer);
StateHasChanged();
}
}
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories, string prefix = "")
private async Task OnThumbnailSelected(IBrowserFile? file)
{
var result = new List<DiscountCategoryDto>();
foreach (var category in categories)
{
var catCopy = new DiscountCategoryDto
{
Id = category.Id,
Title = prefix + category.Title,
ParentCategoryId = category.ParentCategoryId
};
result.Add(catCopy);
if (category.Children.Any())
{
result.AddRange(FlattenCategories(category.Children.ToList(), prefix + " "));
}
}
return result;
}
private string GetCategoryPath(DiscountCategoryDto category)
{
return category.Title;
_thumbnailImageFile = file;
}
private async Task Submit()
@@ -193,6 +206,26 @@
// Update CategoryIds from selected values
Model.CategoryIds = _selectedCategoryIds.ToList();
// Handle main image upload
if (_mainImageFile != null)
{
var buffer = new byte[_mainImageFile.Size];
await _mainImageFile.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
Model.ImageFile = buffer;
Model.ImageMime = _mainImageFile.ContentType;
Model.ImageFileName = Path.GetFileNameWithoutExtension(_mainImageFile.Name);
}
// Handle thumbnail upload
if (_thumbnailImageFile != null)
{
var buffer = new byte[_thumbnailImageFile.Size];
await _thumbnailImageFile.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
Model.ThumbnailFile = buffer;
Model.ThumbnailMime = _thumbnailImageFile.ContentType;
Model.ThumbnailFileName = Path.GetFileNameWithoutExtension(_thumbnailImageFile.Name);
}
MudDialog.Close(DialogResult.Ok(Model));
_loading = false;
}
@@ -211,9 +244,16 @@
public string? ThumbnailPath { get; set; }
public long Price { get; set; }
public int MaxDiscountPercent { get; set; }
public int InitialCount { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
public List<long>? CategoryIds { get; set; }
// Image upload properties
public byte[]? ImageFile { get; set; }
public string? ImageMime { get; set; }
public string? ImageFileName { get; set; }
public byte[]? ThumbnailFile { get; set; }
public string? ThumbnailMime { get; set; }
public string? ThumbnailFileName { get; set; }
}
}
@@ -232,10 +232,16 @@
ThumbnailPath = formData.ThumbnailPath,
Price = formData.Price,
MaxDiscountPercent = formData.MaxDiscountPercent,
InitialCount = formData.InitialCount,
InitialCount = 0, // Always 0 - must use inventory management
SortOrder = formData.SortOrder,
IsActive = formData.IsActive,
CategoryIds = formData.CategoryIds
CategoryIds = formData.CategoryIds,
ImageFile = formData.ImageFile,
ImageMime = formData.ImageMime,
ImageFileName = formData.ImageFileName,
ThumbnailFile = formData.ThumbnailFile,
ThumbnailMime = formData.ThumbnailMime,
ThumbnailFileName = formData.ThumbnailFileName
};
await DiscountProductService.CreateAsync(dto);
@@ -269,7 +275,6 @@
ThumbnailPath = product.ThumbnailPath,
Price = product.Price,
MaxDiscountPercent = product.MaxDiscountPercent,
InitialCount = product.RemainingCount,
SortOrder = product.SortOrder,
IsActive = product.IsActive,
CategoryIds = product.Categories?.Select(c => c.Id).ToList() ?? new()
@@ -298,7 +303,13 @@
MaxDiscountPercent = formData.MaxDiscountPercent,
SortOrder = formData.SortOrder,
IsActive = formData.IsActive,
CategoryIds = formData.CategoryIds
CategoryIds = formData.CategoryIds,
ImageFile = formData.ImageFile,
ImageMime = formData.ImageMime,
ImageFileName = formData.ImageFileName,
ThumbnailFile = formData.ThumbnailFile,
ThumbnailMime = formData.ThumbnailMime,
ThumbnailFileName = formData.ThumbnailFileName
};
await DiscountProductService.UpdateAsync(productId, dto);
@@ -1,4 +1,5 @@
using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.Services.DiscountProduct;
@@ -115,6 +116,28 @@ public class DiscountProductService : IDiscountProductService
request.CategoryIds.AddRange(dto.CategoryIds);
}
// Add image file if provided
if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime) && !string.IsNullOrEmpty(dto.ImageFileName))
{
request.ImageFile = new ImageFileModel
{
File = ByteString.CopyFrom(dto.ImageFile),
Mime = dto.ImageMime,
FileName = dto.ImageFileName
};
}
// Add thumbnail file if provided
if (dto.ThumbnailFile != null && !string.IsNullOrEmpty(dto.ThumbnailMime) && !string.IsNullOrEmpty(dto.ThumbnailFileName))
{
request.ThumbnailFile = new ImageFileModel
{
File = ByteString.CopyFrom(dto.ThumbnailFile),
Mime = dto.ThumbnailMime,
FileName = dto.ThumbnailFileName
};
}
var response = await _client.CreateDiscountProductAsync(request);
return response.ProductId;
}
@@ -140,6 +163,50 @@ public class DiscountProductService : IDiscountProductService
request.CategoryIds.AddRange(dto.CategoryIds);
}
// Handle image file upload
if (dto.ImageFile != null)
{
request.ImageFile = new ImageFileModel
{
File = ByteString.CopyFrom(dto.ImageFile),
Mime = dto.ImageMime ?? string.Empty,
FileName = dto.ImageFileName ?? string.Empty
};
}
// Handle thumbnail file upload
if (dto.ThumbnailFile != null)
{
request.ThumbnailFile = new ImageFileModel
{
File = ByteString.CopyFrom(dto.ThumbnailFile),
Mime = dto.ThumbnailMime ?? string.Empty,
FileName = dto.ThumbnailFileName ?? string.Empty
};
}
// Add image file if provided
if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime) && !string.IsNullOrEmpty(dto.ImageFileName))
{
request.ImageFile = new ImageFileModel
{
File = ByteString.CopyFrom(dto.ImageFile),
Mime = dto.ImageMime,
FileName = dto.ImageFileName
};
}
// Add thumbnail file if provided
if (dto.ThumbnailFile != null && !string.IsNullOrEmpty(dto.ThumbnailMime) && !string.IsNullOrEmpty(dto.ThumbnailFileName))
{
request.ThumbnailFile = new ImageFileModel
{
File = ByteString.CopyFrom(dto.ThumbnailFile),
Mime = dto.ThumbnailMime,
FileName = dto.ThumbnailFileName
};
}
await _client.UpdateDiscountProductAsync(request);
}
@@ -74,6 +74,14 @@ public class CreateDiscountProductDto
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
public List<long>? CategoryIds { get; set; }
// Image upload support
public byte[]? ImageFile { get; set; }
public string? ImageMime { get; set; }
public string? ImageFileName { get; set; }
public byte[]? ThumbnailFile { get; set; }
public string? ThumbnailMime { get; set; }
public string? ThumbnailFileName { get; set; }
}
public class UpdateDiscountProductDto
@@ -88,4 +96,13 @@ public class UpdateDiscountProductDto
public int SortOrder { get; set; }
public bool IsActive { get; set; }
public List<long>? CategoryIds { get; set; }
// Image upload support
public byte[]? ImageFile { get; set; }
public string? ImageMime { get; set; }
public string? ImageFileName { get; set; }
public byte[]? ThumbnailFile { get; set; }
public string? ThumbnailMime { get; set; }
public string? ThumbnailFileName { get; set; }
}