feat: Add SitePageSettings management UI
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:
masoodafar-web
2026-02-17 22:34:59 +03:30
parent af51c02d3d
commit 3409133757
13 changed files with 1029 additions and 3 deletions
+1 -1
View File
@@ -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 });
}
}
+2 -2
View File
@@ -231,9 +231,9 @@
@if (CanManageSitePages)
{
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/content/pages"
Href="/content/page-settings"
Icon="@Icons.Material.Filled.WebAsset">
صفحات سایت
تنظیمات صفحات
</MudNavLink>
}