(
+ "ویرایش تصویر", 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();
+ }
+}
diff --git a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor
new file mode 100644
index 0000000..899e7be
--- /dev/null
+++ b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor
@@ -0,0 +1,88 @@
+@using BackOffice.Services.Content
+@using Microsoft.AspNetCore.Components.Forms
+@using BackOffice.Common.BaseComponents
+
+
+
+
+
+
+
+
+
+
+
+
+
+ تصویر هیرو
+
+ @if (!string.IsNullOrWhiteSpace(_imagePreview))
+ {
+
+ }
+ else if (!string.IsNullOrEmpty(ExistingHeroImagePath))
+ {
+
+ }
+ else
+ {
+
+ تصویری انتخاب نشده است
+
+ }
+
+
+
+
+ انتخاب تصویر هیرو
+
+
+
+ @if (context != null)
+ {
+ @context.Name
+ }
+
+
+
+
+
+
+ تنظیمات اختصاصی (JSON)
+
+
+
+
+ فعال
+
+
+
+
+
+
+ ذخیره تغییرات
+
+
+ انصراف
+
+
+
diff --git a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs
new file mode 100644
index 0000000..028a351
--- /dev/null
+++ b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs
@@ -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();
+ }
+}
diff --git a/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor
new file mode 100644
index 0000000..8a99432
--- /dev/null
+++ b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor
@@ -0,0 +1,74 @@
+@page "/content/page-settings"
+
+@using BackOffice.Services.Content
+
+@inject ISitePageSettingsService PageSettingsService
+
+
+
+ مدیریت تنظیمات صفحات
+ تنظیمات ۴ صفحه ثابت سایت: صفحه اصلی، درباره ما، تماس با ما و مجوزها
+
+ @if (_loading)
+ {
+
+ }
+ else
+ {
+
+ @foreach (var item in _pages)
+ {
+
+
+
+
+
+
+
+ @item.Title
+ @item.PageKey
+
+
+
+
+
+ @(item.IsActive ? "فعال" : "غیرفعال")
+
+
+
+
+
+
+
+ @item.ImageCount تصویر
+
+
+ آخرین ویرایش: @item.LastModified.MiladiToJalaliWithTime()
+
+
+
+
+
+ ویرایش
+
+
+ تصاویر
+
+
+
+
+ }
+
+ }
+
+
diff --git a/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor.cs b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor.cs
new file mode 100644
index 0000000..6c77e93
--- /dev/null
+++ b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor.cs
@@ -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 _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
+ {
+ { nameof(PageSettingsEditDialog.Model), dto },
+ { nameof(PageSettingsEditDialog.ExistingHeroImagePath), details.HeroImagePath },
+ { nameof(PageSettingsEditDialog.PageTitle), GetPageDisplayName(summary.PageKey) }
+ };
+
+ var dialog = await DialogService.ShowAsync(
+ $"ویرایش تنظیمات «{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
+ {
+ { 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(
+ $"مدیریت تصاویر «{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
+ };
+}
diff --git a/src/BackOffice/Services/Content/ISitePageSettingsService.cs b/src/BackOffice/Services/Content/ISitePageSettingsService.cs
new file mode 100644
index 0000000..dca372c
--- /dev/null
+++ b/src/BackOffice/Services/Content/ISitePageSettingsService.cs
@@ -0,0 +1,96 @@
+namespace BackOffice.Services.Content;
+
+public interface ISitePageSettingsService
+{
+ Task> GetAllAsync();
+ Task GetByKeyAsync(string pageKey);
+ Task SaveSettingsAsync(SavePageSettingsDto dto);
+ Task 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 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; }
+}
diff --git a/src/BackOffice/Services/Content/SitePageSettingsService.cs b/src/BackOffice/Services/Content/SitePageSettingsService.cs
new file mode 100644
index 0000000..40f94f4
--- /dev/null
+++ b/src/BackOffice/Services/Content/SitePageSettingsService.cs
@@ -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> 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 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 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 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 });
+ }
+}
diff --git a/src/BackOffice/Shared/NavMenu.razor b/src/BackOffice/Shared/NavMenu.razor
index 44f964a..6a61a93 100644
--- a/src/BackOffice/Shared/NavMenu.razor
+++ b/src/BackOffice/Shared/NavMenu.razor
@@ -231,9 +231,9 @@
@if (CanManageSitePages)
{
- صفحات سایت
+ تنظیمات صفحات
}