feat: Add SitePageSettings management UI
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m21s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m21s
- ISitePageSettingsService + SitePageSettingsService (gRPC client) - PageSettingsManagementPage: 4 cards for fixed pages - PageSettingsEditDialog: edit title, hero, meta, settingsJson - PageImagesDialog: CRUD for page images (licenses, team, values) - PageImageEditDialog: add/edit single image with upload - Proto NuGet updated to 0.0.180 - NavMenu: added link to page settings
This commit is contained in:
@@ -138,7 +138,7 @@
|
||||
|
||||
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.178" />
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.180" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
|
||||
@@ -27,6 +27,7 @@ using CMSMicroservice.Protobuf.Protos.BlogPost;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogCategory;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogPostImage;
|
||||
using CMSMicroservice.Protobuf.Protos.SitePage;
|
||||
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
|
||||
using CMSMicroservice.Protobuf.Protos.UserContract;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWallet;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
|
||||
@@ -91,6 +92,7 @@ public static class ConfigureServices
|
||||
services.AddScoped<IBlogCategoryService, BlogCategoryService>();
|
||||
services.AddScoped<IBlogPostImageService, BlogPostImageService>();
|
||||
services.AddScoped<ISitePageService, SitePageService>();
|
||||
services.AddScoped<ISitePageSettingsService, SitePageSettingsService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -158,6 +160,7 @@ public static class ConfigureServices
|
||||
services.AddTransient(sp => new BlogCategoryContract.BlogCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new BlogPostImageContract.BlogPostImageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new SitePageContract.SitePageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new SitePageSettingsContract.SitePageSettingsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// User Contracts Service
|
||||
services.AddTransient(sp => new UserContractContract.UserContractContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
|
||||
<MudSelect T="string" @bind-Value="Model.ImageGroup"
|
||||
Label="گروه تصویر"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined">
|
||||
@foreach (var group in GetAvailableGroups())
|
||||
{
|
||||
<MudSelectItem T="string" Value="@group.Key">@group.Value</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Subtitle"
|
||||
Label="زیرعنوان"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
|
||||
<MudTextField @bind-Value="Model.LinkUrl"
|
||||
Label="لینک"
|
||||
Variant="Variant.Outlined"
|
||||
Placeholder="https://..." />
|
||||
|
||||
<MudTextField @bind-Value="Model.IconName"
|
||||
Label="نام آیکون"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="نام آیکون Material Design — مثلاً: Verified, Star, Group" />
|
||||
|
||||
<MudNumericField @bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined"
|
||||
Min="0" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="200" Height="150" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(ExistingImagePath))
|
||||
{
|
||||
<Image Src="@ExistingImagePath" Width="200" Height="150" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:200px;height:150px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync"
|
||||
Disabled="@(!_isValid || _loading)">
|
||||
@(IsNew ? "افزودن" : "ذخیره تغییرات")
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,112 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class PageImageEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SavePageImageDto Model { get; set; } = new();
|
||||
[Parameter] public string PageKey { get; set; } = string.Empty;
|
||||
[Parameter] public bool IsNew { get; set; }
|
||||
[Parameter] public string? ExistingImagePath { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
else if (IsNew && _imageBuffer == null)
|
||||
{
|
||||
Snackbar.Add("لطفاً یک تصویر انتخاب کنید", Severity.Warning);
|
||||
_loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await PageSettingsService.SaveImageAsync(Model);
|
||||
if (result.Success)
|
||||
{
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("خطا در ذخیره تصویر", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> GetAvailableGroups() => PageKey switch
|
||||
{
|
||||
"about" => new Dictionary<string, string>
|
||||
{
|
||||
{ "team", "اعضای تیم" },
|
||||
{ "values", "ارزشها" }
|
||||
},
|
||||
"licenses" => new Dictionary<string, string>
|
||||
{
|
||||
{ "licenses", "مجوزها" }
|
||||
},
|
||||
"landing" => new Dictionary<string, string>
|
||||
{
|
||||
{ "values", "ارزشها" }
|
||||
},
|
||||
_ => new Dictionary<string, string>
|
||||
{
|
||||
{ "values", "ارزشها" },
|
||||
{ "team", "اعضای تیم" },
|
||||
{ "licenses", "مجوزها" }
|
||||
}
|
||||
};
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle1">تصاویر صفحه «@PageTitle»</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddImage">
|
||||
افزودن تصویر
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@if (!Images.Any())
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
|
||||
تصویری ثبت نشده است.
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="PageImageItemDto"
|
||||
Items="Images"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Elevation="0">
|
||||
<Columns>
|
||||
<TemplateColumn Title="تصویر" Sortable="false">
|
||||
<CellTemplate>
|
||||
@if (!string.IsNullOrWhiteSpace(context.Item.ThumbnailPath))
|
||||
{
|
||||
<Image Src="@context.Item.ThumbnailPath" Width="48" Height="48" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(context.Item.ImagePath))
|
||||
{
|
||||
<Image Src="@context.Item.ImagePath" Width="48" Height="48" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.ImageNotSupported" Size="Size.Medium" />
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.ImageGroup" Title="گروه" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditImage(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeleteImage(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Close">
|
||||
بستن
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,133 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class PageImagesDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!;
|
||||
|
||||
[Parameter] public long PageId { get; set; }
|
||||
[Parameter] public string PageKey { get; set; } = string.Empty;
|
||||
[Parameter] public string PageTitle { get; set; } = string.Empty;
|
||||
[Parameter] public List<PageImageItemDto> Images { get; set; } = new();
|
||||
|
||||
private bool _hasChanges;
|
||||
|
||||
private async Task AddImage()
|
||||
{
|
||||
var dto = new SavePageImageDto
|
||||
{
|
||||
SitePageSettingsId = PageId,
|
||||
ImageGroup = GetDefaultImageGroup(),
|
||||
IsActive = true,
|
||||
SortOrder = Images.Count + 1
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<PageImageEditDialog>
|
||||
{
|
||||
{ nameof(PageImageEditDialog.Model), dto },
|
||||
{ nameof(PageImageEditDialog.PageKey), PageKey },
|
||||
{ nameof(PageImageEditDialog.IsNew), true }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<PageImageEditDialog>(
|
||||
"افزودن تصویر", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
Snackbar.Add("تصویر با موفقیت اضافه شد", Severity.Success);
|
||||
_hasChanges = true;
|
||||
await ReloadImages();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditImage(PageImageItemDto item)
|
||||
{
|
||||
var dto = new SavePageImageDto
|
||||
{
|
||||
Id = item.Id,
|
||||
SitePageSettingsId = PageId,
|
||||
ImageGroup = item.ImageGroup,
|
||||
Title = item.Title,
|
||||
Subtitle = item.Subtitle,
|
||||
Description = item.Description,
|
||||
LinkUrl = item.LinkUrl,
|
||||
IconName = item.IconName,
|
||||
SortOrder = item.SortOrder,
|
||||
IsActive = item.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<PageImageEditDialog>
|
||||
{
|
||||
{ nameof(PageImageEditDialog.Model), dto },
|
||||
{ nameof(PageImageEditDialog.PageKey), PageKey },
|
||||
{ nameof(PageImageEditDialog.IsNew), false },
|
||||
{ nameof(PageImageEditDialog.ExistingImagePath), item.ImagePath }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<PageImageEditDialog>(
|
||||
"ویرایش تصویر", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
Snackbar.Add("تصویر با موفقیت ویرایش شد", Severity.Success);
|
||||
_hasChanges = true;
|
||||
await ReloadImages();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteImage(PageImageItemDto item)
|
||||
{
|
||||
var confirm = await DialogService.ShowMessageBox(
|
||||
"تایید حذف",
|
||||
$"آیا از حذف تصویر «{item.Title ?? item.ImageGroup}» اطمینان دارید؟",
|
||||
yesText: "بله، حذف شود",
|
||||
cancelText: "انصراف");
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try
|
||||
{
|
||||
await PageSettingsService.DeleteImageAsync(item.Id);
|
||||
Snackbar.Add("تصویر با موفقیت حذف شد", Severity.Success);
|
||||
_hasChanges = true;
|
||||
await ReloadImages();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف تصویر: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadImages()
|
||||
{
|
||||
var details = await PageSettingsService.GetByKeyAsync(PageKey);
|
||||
if (details != null)
|
||||
{
|
||||
Images = details.Images;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDefaultImageGroup() => PageKey switch
|
||||
{
|
||||
"about" => "team",
|
||||
"licenses" => "licenses",
|
||||
_ => "values"
|
||||
};
|
||||
|
||||
private void Close()
|
||||
{
|
||||
if (_hasChanges)
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
else
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان صفحه"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.MetaDescription"
|
||||
Label="توضیحات متا (SEO)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroTitle"
|
||||
Label="عنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroSubtitle"
|
||||
Label="زیرعنوان هیرو"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر هیرو</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(ExistingHeroImagePath))
|
||||
{
|
||||
<Image Src="@ExistingHeroImagePath" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:160px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر هیرو
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudDivider Class="my-2" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تنظیمات اختصاصی (JSON)</MudText>
|
||||
<MudTextField @bind-Value="Model.SettingsJson"
|
||||
Label="تنظیمات JSON"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="8"
|
||||
HelperText="تنظیمات اختصاصی هر صفحه — مثلاً خدمات، مراحل، FAQ و..." />
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
ذخیره تغییرات
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,82 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class PageSettingsEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SavePageSettingsDto Model { get; set; } = new();
|
||||
[Parameter] public string? ExistingHeroImagePath { get; set; }
|
||||
[Parameter] public string PageTitle { get; set; } = string.Empty;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await PageSettingsService.SaveSettingsAsync(Model);
|
||||
if (result.Success)
|
||||
{
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("خطا در ذخیره تنظیمات", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@page "/content/page-settings"
|
||||
|
||||
@using BackOffice.Services.Content
|
||||
|
||||
@inject ISitePageSettingsService PageSettingsService
|
||||
|
||||
<div>
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.h5">مدیریت تنظیمات صفحات</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">تنظیمات ۴ صفحه ثابت سایت: صفحه اصلی، درباره ما، تماس با ما و مجوزها</MudText>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudGrid>
|
||||
@foreach (var item in _pages)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2" Class="pa-2">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@GetPageIcon(item.PageKey)" Size="Size.Large" Color="Color.Primary" />
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.h6">@item.Title</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@item.PageKey</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudChip T="string"
|
||||
Color="@(item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Image" Size="Size.Small" Class="ml-1" />
|
||||
@item.ImageCount تصویر
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
آخرین ویرایش: @item.LastModified.MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Edit"
|
||||
OnClick="@(() => EditPageSettings(item))">
|
||||
ویرایش
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Collections"
|
||||
OnClick="@(() => ManageImages(item))">
|
||||
تصاویر
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
}
|
||||
</MudStack>
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
using BackOffice.Services.Content;
|
||||
using BackOffice.Pages.Content.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content;
|
||||
|
||||
public partial class PageSettingsManagementPage
|
||||
{
|
||||
private List<PageSettingsSummaryDto> _pages = new();
|
||||
private bool _loading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadPagesAsync()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
_pages = await PageSettingsService.GetAllAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در دریافت صفحات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditPageSettings(PageSettingsSummaryDto summary)
|
||||
{
|
||||
var details = await PageSettingsService.GetByKeyAsync(summary.PageKey);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت تنظیمات صفحه", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var dto = new SavePageSettingsDto
|
||||
{
|
||||
PageKey = details.PageKey,
|
||||
Title = details.Title,
|
||||
MetaDescription = details.MetaDescription,
|
||||
HeroTitle = details.HeroTitle,
|
||||
HeroSubtitle = details.HeroSubtitle,
|
||||
IsActive = details.IsActive,
|
||||
SettingsJson = details.SettingsJson
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<PageSettingsEditDialog>
|
||||
{
|
||||
{ nameof(PageSettingsEditDialog.Model), dto },
|
||||
{ nameof(PageSettingsEditDialog.ExistingHeroImagePath), details.HeroImagePath },
|
||||
{ nameof(PageSettingsEditDialog.PageTitle), GetPageDisplayName(summary.PageKey) }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<PageSettingsEditDialog>(
|
||||
$"ویرایش تنظیمات «{GetPageDisplayName(summary.PageKey)}»", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
Snackbar.Add("تنظیمات صفحه با موفقیت ذخیره شد", Severity.Success);
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ManageImages(PageSettingsSummaryDto summary)
|
||||
{
|
||||
var details = await PageSettingsService.GetByKeyAsync(summary.PageKey);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت اطلاعات صفحه", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<PageImagesDialog>
|
||||
{
|
||||
{ nameof(PageImagesDialog.PageId), details.Id },
|
||||
{ nameof(PageImagesDialog.PageKey), details.PageKey },
|
||||
{ nameof(PageImagesDialog.PageTitle), GetPageDisplayName(summary.PageKey) },
|
||||
{ nameof(PageImagesDialog.Images), details.Images }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<PageImagesDialog>(
|
||||
$"مدیریت تصاویر «{GetPageDisplayName(summary.PageKey)}»", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetPageIcon(string pageKey) => pageKey switch
|
||||
{
|
||||
"landing" => Icons.Material.Filled.Home,
|
||||
"about" => Icons.Material.Filled.Info,
|
||||
"contact" => Icons.Material.Filled.ContactMail,
|
||||
"licenses" => Icons.Material.Filled.VerifiedUser,
|
||||
_ => Icons.Material.Filled.Article
|
||||
};
|
||||
|
||||
private static string GetPageDisplayName(string pageKey) => pageKey switch
|
||||
{
|
||||
"landing" => "صفحه اصلی",
|
||||
"about" => "درباره ما",
|
||||
"contact" => "تماس با ما",
|
||||
"licenses" => "مجوزها",
|
||||
_ => pageKey
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
namespace BackOffice.Services.Content;
|
||||
|
||||
public interface ISitePageSettingsService
|
||||
{
|
||||
Task<List<PageSettingsSummaryDto>> GetAllAsync();
|
||||
Task<PageSettingsDetailsDto?> GetByKeyAsync(string pageKey);
|
||||
Task<SaveResultDto> SaveSettingsAsync(SavePageSettingsDto dto);
|
||||
Task<SaveResultDto> SaveImageAsync(SavePageImageDto dto);
|
||||
Task DeleteImageAsync(long id);
|
||||
}
|
||||
|
||||
// ── Summary DTO (لیست صفحات) ──
|
||||
public class PageSettingsSummaryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string PageKey { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public int ImageCount { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
}
|
||||
|
||||
// ── Details DTO (تنظیمات کامل + تصاویر) ──
|
||||
public class PageSettingsDetailsDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string PageKey { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? MetaDescription { get; set; }
|
||||
public string? HeroTitle { get; set; }
|
||||
public string? HeroSubtitle { get; set; }
|
||||
public string? HeroImagePath { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string? SettingsJson { get; set; }
|
||||
public List<PageImageItemDto> Images { get; set; } = new();
|
||||
}
|
||||
|
||||
// ── Image Item DTO ──
|
||||
public class PageImageItemDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string ImageGroup { get; set; } = string.Empty;
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public string? LinkUrl { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
// ── Save Settings DTO ──
|
||||
public class SavePageSettingsDto
|
||||
{
|
||||
public string PageKey { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? MetaDescription { get; set; }
|
||||
public string? HeroTitle { get; set; }
|
||||
public string? HeroSubtitle { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public string? SettingsJson { get; set; }
|
||||
|
||||
// Hero image upload
|
||||
public byte[]? ImageFile { get; set; }
|
||||
public string? ImageMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
}
|
||||
|
||||
// ── Save Image DTO ──
|
||||
public class SavePageImageDto
|
||||
{
|
||||
public long Id { get; set; } // 0 = جدید
|
||||
public long SitePageSettingsId { get; set; }
|
||||
public string ImageGroup { get; set; } = string.Empty;
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? LinkUrl { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Image upload
|
||||
public byte[]? ImageFile { get; set; }
|
||||
public string? ImageMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
}
|
||||
|
||||
// ── Save Result ──
|
||||
public class SaveResultDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
|
||||
|
||||
namespace BackOffice.Services.Content;
|
||||
|
||||
public class SitePageSettingsService : ISitePageSettingsService
|
||||
{
|
||||
private readonly SitePageSettingsContract.SitePageSettingsContractClient _client;
|
||||
|
||||
public SitePageSettingsService(SitePageSettingsContract.SitePageSettingsContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<List<PageSettingsSummaryDto>> GetAllAsync()
|
||||
{
|
||||
var response = await _client.GetAllPageSettingsAsync(new GetAllPageSettingsRequest());
|
||||
|
||||
return response.Pages.Select(m => new PageSettingsSummaryDto
|
||||
{
|
||||
Id = m.Id,
|
||||
PageKey = m.PageKey,
|
||||
Title = m.Title,
|
||||
IsActive = m.IsActive,
|
||||
ImageCount = m.ImageCount,
|
||||
LastModified = m.LastModified?.ToDateTime() ?? DateTime.MinValue
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<PageSettingsDetailsDto?> GetByKeyAsync(string pageKey)
|
||||
{
|
||||
var response = await _client.GetPageSettingsAsync(
|
||||
new GetPageSettingsRequest { PageKey = pageKey });
|
||||
|
||||
if (response == null || response.Id <= 0) return null;
|
||||
|
||||
var dto = new PageSettingsDetailsDto
|
||||
{
|
||||
Id = response.Id,
|
||||
PageKey = response.PageKey,
|
||||
Title = response.Title,
|
||||
MetaDescription = response.MetaDescription,
|
||||
HeroTitle = response.HeroTitle,
|
||||
HeroSubtitle = response.HeroSubtitle,
|
||||
HeroImagePath = response.HeroImagePath,
|
||||
IsActive = response.IsActive,
|
||||
SettingsJson = response.SettingsJson
|
||||
};
|
||||
|
||||
foreach (var img in response.Images)
|
||||
{
|
||||
dto.Images.Add(new PageImageItemDto
|
||||
{
|
||||
Id = img.Id,
|
||||
ImageGroup = img.ImageGroup,
|
||||
Title = img.Title,
|
||||
Subtitle = img.Subtitle,
|
||||
Description = img.Description,
|
||||
ImagePath = img.ImagePath,
|
||||
ThumbnailPath = img.ThumbnailPath,
|
||||
LinkUrl = img.LinkUrl,
|
||||
IconName = img.IconName,
|
||||
SortOrder = img.SortOrder,
|
||||
IsActive = img.IsActive
|
||||
});
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<SaveResultDto> SaveSettingsAsync(SavePageSettingsDto dto)
|
||||
{
|
||||
var request = new SavePageSettingsRequest
|
||||
{
|
||||
PageKey = dto.PageKey,
|
||||
Title = dto.Title,
|
||||
MetaDescription = dto.MetaDescription ?? string.Empty,
|
||||
HeroTitle = dto.HeroTitle ?? string.Empty,
|
||||
HeroSubtitle = dto.HeroSubtitle ?? string.Empty,
|
||||
IsActive = dto.IsActive,
|
||||
SettingsJson = dto.SettingsJson ?? string.Empty
|
||||
};
|
||||
|
||||
if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime) && !string.IsNullOrEmpty(dto.ImageFileName))
|
||||
{
|
||||
request.HeroImageFile = new PageSettingsImageFile
|
||||
{
|
||||
File = Google.Protobuf.ByteString.CopyFrom(dto.ImageFile),
|
||||
Mime = dto.ImageMime,
|
||||
FileName = dto.ImageFileName
|
||||
};
|
||||
}
|
||||
|
||||
var response = await _client.SavePageSettingsAsync(request);
|
||||
return new SaveResultDto { Id = response.Id, Success = response.Success };
|
||||
}
|
||||
|
||||
public async Task<SaveResultDto> SaveImageAsync(SavePageImageDto dto)
|
||||
{
|
||||
var request = new SavePageImageRequest
|
||||
{
|
||||
Id = dto.Id,
|
||||
SitePageSettingsId = dto.SitePageSettingsId,
|
||||
ImageGroup = dto.ImageGroup,
|
||||
Title = dto.Title ?? string.Empty,
|
||||
Subtitle = dto.Subtitle ?? string.Empty,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
LinkUrl = dto.LinkUrl ?? string.Empty,
|
||||
IconName = dto.IconName ?? string.Empty,
|
||||
SortOrder = dto.SortOrder,
|
||||
IsActive = dto.IsActive
|
||||
};
|
||||
|
||||
if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime) && !string.IsNullOrEmpty(dto.ImageFileName))
|
||||
{
|
||||
request.ImageFile = new PageSettingsImageFile
|
||||
{
|
||||
File = Google.Protobuf.ByteString.CopyFrom(dto.ImageFile),
|
||||
Mime = dto.ImageMime,
|
||||
FileName = dto.ImageFileName
|
||||
};
|
||||
}
|
||||
|
||||
var response = await _client.SavePageImageAsync(request);
|
||||
return new SaveResultDto { Id = response.Id, Success = response.Success };
|
||||
}
|
||||
|
||||
public async Task DeleteImageAsync(long id)
|
||||
{
|
||||
await _client.DeletePageImageAsync(new DeletePageImageRequest { Id = id });
|
||||
}
|
||||
}
|
||||
@@ -231,9 +231,9 @@
|
||||
@if (CanManageSitePages)
|
||||
{
|
||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||
Href="/content/pages"
|
||||
Href="/content/page-settings"
|
||||
Icon="@Icons.Material.Filled.WebAsset">
|
||||
صفحات سایت
|
||||
تنظیمات صفحات
|
||||
</MudNavLink>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user