feat: add image and thumbnail upload support in DiscountProduct forms and services
Build and Deploy / build (push) Failing after 17s
Build and Deploy / build (push) Failing after 17s
This commit is contained in:
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"
|
||||
Label="عنوان محصول *"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<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>
|
||||
}
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.ShortInformation"
|
||||
Label="توضیح کوتاه"
|
||||
Lines="2"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<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>
|
||||
|
||||
<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"
|
||||
Label="قیمت (ریال) *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Format="N0"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.MaxDiscountPercent"
|
||||
Label="حداکثر درصد تخفیف *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Max="100"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.SortOrder"
|
||||
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)
|
||||
{
|
||||
<MudSelectItem Value="@category.Id">@GetCategoryPath(category)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="Model.ImagePath"
|
||||
Label="مسیر تصویر اصلی"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Image" />
|
||||
</MudItem>
|
||||
|
||||
<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" />
|
||||
<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"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
}
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="long" HideSpinButtons="true"
|
||||
@bind-Value="Model.Price"
|
||||
Format="N0"
|
||||
Immediate="true"
|
||||
Disabled="_loading"
|
||||
Label="قیمت (ریال) *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive"
|
||||
Label="محصول فعال"
|
||||
Color="Color.Success" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<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"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="int" HideSpinButtons="true"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Disabled="_loading"
|
||||
Label="ترتیب نمایش"
|
||||
Min="0"
|
||||
Variant="Variant.Outlined"
|
||||
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)
|
||||
{
|
||||
<MudText Typo="Typo.caption">
|
||||
تعداد دستهبندیهای انتخابشده: @_selectedCategoryIds.Count
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تصویر بندانگشتی</MudText>
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnThumbnailSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
انتخاب تصویر بندانگشتی (اختیاری)
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive"
|
||||
Label="محصول فعال"
|
||||
Color="Color.Success" />
|
||||
</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; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user