merge: kub-stage → production (discount autocomplete + smart AddStockDialog + site pages admin)
Build and Deploy to Production / build-and-deploy (push) Successful in 8m9s
Build and Deploy to Production / build-and-deploy (push) Successful in 8m9s
This commit is contained in:
@@ -25,6 +25,24 @@
|
||||
- **تغییر:** `Foursat.CMSMicroservice.Protobuf` → `0.0.179`
|
||||
- **فایل:** `BackOffice.csproj`
|
||||
|
||||
### ۱۵.۴ — فیکس K8S_SERVER در prod-deploy.yml ✅
|
||||
|
||||
- **مشکل:** `K8S_SERVER` هنوز `194.5.195.53` (staging) بود
|
||||
- **رفع:** تغییر به `45.149.79.127` (production)
|
||||
- **فیکس اضافی:** `kubectl rollout restart` → `kubectl set image` (تا ایمیج SHA-tagged واقعاً set بشه)
|
||||
|
||||
### ۱۵.۵ — ساخت appsettings.Production.json ✅
|
||||
|
||||
- **مشکل:** فایل `appsettings.Production.json` وجود نداشت → `GwUrl` fallback به `localhost`
|
||||
- **رفع:** ساخت `wwwroot/appsettings.Production.json` با `GwUrl=https://cms.kbs1.ir`
|
||||
|
||||
### ۱۵.۶ — فیکس nginx image path در Dockerfile (production branch) ✅
|
||||
|
||||
- **مشکل:** Dockerfile روی برنچ production از `library/nginx:alpine` استفاده میکرد — وجود نداشت
|
||||
- **ارور:** `manifest for 194.5.195.53:32082/library/nginx:alpine not found`
|
||||
- **رفع:** تغییر به `194.5.195.53:32082/nginx:alpine`
|
||||
- **کامیت:** `743403e` (مستقیم روی برنچ production)
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۱۴ — فیکس Docker Build + Deployment (بهمن ۱۴۰۴)
|
||||
|
||||
@@ -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,14 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.DiscountProduct
|
||||
|
||||
<MudAutocomplete T="DiscountProductDto"
|
||||
Label="@Label"
|
||||
Value="@_item"
|
||||
DebounceInterval="700"
|
||||
ValueChanged="@((e) => OnSelected(e))"
|
||||
ToStringFunc="@(e => e == null ? null : e.Title)"
|
||||
SearchFunc="@Search"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
ShowProgressIndicator="true"
|
||||
CoerceText="false"
|
||||
OnClearButtonClick="()=> OnSelected(null)" />
|
||||
@@ -0,0 +1,83 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
public partial class DiscountProductsAutoComplete
|
||||
{
|
||||
[Inject] public DiscountProductContract.DiscountProductContractClient DiscountProductService { get; set; } = default!;
|
||||
|
||||
[Parameter] public long? Value { get; set; }
|
||||
[Parameter] public string? Label { get; set; } = "محصول تخفیفی";
|
||||
[Parameter] public EventCallback<long?> ValueChanged { get; set; }
|
||||
|
||||
private List<DiscountProductDto> _items = new();
|
||||
private DiscountProductDto? _item;
|
||||
|
||||
protected override async void OnParametersSet()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
if (Value.HasValue && Value.Value > 0 && (_item == null || _item.Id != Value.Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
var getAll = await DiscountProductService.GetDiscountProductsAsync(new GetDiscountProductsRequest
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 50
|
||||
});
|
||||
|
||||
if (getAll?.Models != null)
|
||||
{
|
||||
_item = getAll.Models.FirstOrDefault(x => x.Id == Value.Value);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently handle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSelected(DiscountProductDto? model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
_item = null;
|
||||
await ValueChanged.InvokeAsync(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_item = model;
|
||||
await ValueChanged.InvokeAsync(model.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<DiscountProductDto>> Search(string value, CancellationToken token)
|
||||
{
|
||||
var request = new GetDiscountProductsRequest
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 9
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
request.SearchQuery = value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var getAll = await DiscountProductService.GetDiscountProductsAsync(request);
|
||||
_items = getAll?.Models?.ToList() ?? new List<DiscountProductDto>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_items = new List<DiscountProductDto>();
|
||||
}
|
||||
|
||||
return _items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
@using BackOffice.Pages.Content.Components.SettingsModels
|
||||
|
||||
<MudExpansionPanels MultiExpansion="true" Elevation="0">
|
||||
|
||||
@* ── Vision ── *@
|
||||
<MudExpansionPanel Text="چشمانداز" Icon="@Icons.Material.Filled.Visibility" IsInitiallyExpanded="true">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.VisionTitle"
|
||||
Label="عنوان"
|
||||
Placeholder="چشمانداز"
|
||||
Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="Model.VisionText"
|
||||
Label="متن چشمانداز"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
<MudTextField @bind-Value="Model.VisionIcon"
|
||||
Label="نام آیکون"
|
||||
Placeholder="Visibility"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="نام آیکون از مجموعه Material Icons" />
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Mission ── *@
|
||||
<MudExpansionPanel Text="مأموریت" Icon="@Icons.Material.Filled.TrackChanges" IsInitiallyExpanded="true">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.MissionTitle"
|
||||
Label="عنوان"
|
||||
Placeholder="مأموریت"
|
||||
Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="Model.MissionText"
|
||||
Label="متن مأموریت"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
<MudTextField @bind-Value="Model.MissionIcon"
|
||||
Label="نام آیکون"
|
||||
Placeholder="TrackChanges"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="نام آیکون از مجموعه Material Icons" />
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Section Titles ── *@
|
||||
<MudExpansionPanel Text="عناوین بخشها" Icon="@Icons.Material.Filled.Title">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.ValuesTitle"
|
||||
Label="عنوان بخش ارزشها"
|
||||
Placeholder="ارزشهای ما"
|
||||
Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="Model.TeamTitle"
|
||||
Label="عنوان بخش تیم"
|
||||
Placeholder="تیم ما"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Hint ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mt-2">
|
||||
آیتمهای ارزشها و اعضای تیم از بخش «مدیریت تصاویر» قابل ویرایش هستند.
|
||||
</MudAlert>
|
||||
|
||||
</MudExpansionPanels>
|
||||
|
||||
@code {
|
||||
[Parameter] public AboutSettingsModel Model { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
@using BackOffice.Pages.Content.Components.SettingsModels
|
||||
|
||||
<MudExpansionPanels MultiExpansion="true" Elevation="0">
|
||||
|
||||
@* ── Contact Info ── *@
|
||||
<MudExpansionPanel Text="اطلاعات تماس" Icon="@Icons.Material.Filled.ContactPhone" IsInitiallyExpanded="true">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.Address"
|
||||
Label="آدرس"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2"
|
||||
Placeholder="کرج مهرویلا میدان مادر..." />
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="Model.Phone"
|
||||
Label="شماره تلفن"
|
||||
Placeholder="026-34233563"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="Model.Email"
|
||||
Label="ایمیل"
|
||||
Placeholder="info@kbs1.co"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<MudTextField @bind-Value="Model.WorkingHours"
|
||||
Label="ساعات کاری"
|
||||
Placeholder="شنبه تا پنجشنبه — ۹ صبح تا ۶ عصر"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Social Media ── *@
|
||||
<MudExpansionPanel Text="شبکههای اجتماعی" Icon="@Icons.Material.Filled.Share" IsInitiallyExpanded="true">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.TelegramUrl"
|
||||
Label="تلگرام"
|
||||
Placeholder="https://t.me/kbs1"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Custom.Brands.Telegram" />
|
||||
<MudTextField @bind-Value="Model.InstagramUrl"
|
||||
Label="اینستاگرام"
|
||||
Placeholder="https://instagram.com/kbs1"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Custom.Brands.Instagram" />
|
||||
<MudTextField @bind-Value="Model.LinkedinUrl"
|
||||
Label="لینکدین"
|
||||
Placeholder="https://linkedin.com/company/kbs1"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Custom.Brands.LinkedIn" />
|
||||
<MudTextField @bind-Value="Model.WhatsappUrl"
|
||||
Label="واتساپ"
|
||||
Placeholder="https://wa.me/989123456789"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Custom.Brands.WhatsApp" />
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
</MudExpansionPanels>
|
||||
|
||||
@code {
|
||||
[Parameter] public ContactSettingsModel Model { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
@using BackOffice.Pages.Content.Components.SettingsModels
|
||||
|
||||
<MudExpansionPanels MultiExpansion="true" Elevation="0">
|
||||
|
||||
@* ── Hero Buttons ── *@
|
||||
<MudExpansionPanel Text="دکمههای هیرو" Icon="@Icons.Material.Filled.TouchApp" IsInitiallyExpanded="true">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.HeroButtonPrimaryText"
|
||||
Label="متن دکمه اصلی"
|
||||
Placeholder="شروع رایگان"
|
||||
Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="Model.HeroButtonSecondaryText"
|
||||
Label="متن دکمه ثانویه"
|
||||
Placeholder="آخرین اخبار"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Trust Badges ── *@
|
||||
<MudExpansionPanel Text="@($"نشانهای اعتماد ({Model.TrustBadges.Count})")" Icon="@Icons.Material.Filled.Verified">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
@for (var i = 0; i < Model.TrustBadges.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
|
||||
<MudTextField @bind-Value="Model.TrustBadges[index].IconName"
|
||||
Label="نام آیکون"
|
||||
Placeholder="Timer"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudTextField @bind-Value="Model.TrustBadges[index].Text"
|
||||
Label="متن"
|
||||
Placeholder="ثبتنام زیر ۲ دقیقه"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:2;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="() => Model.TrustBadges.RemoveAt(index)" />
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Text"
|
||||
OnClick="() => Model.TrustBadges.Add(new TrustBadgeModel())">
|
||||
افزودن نشان
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Steps ── *@
|
||||
<MudExpansionPanel Text="@($"مراحل ({Model.Steps.Count})")" Icon="@Icons.Material.Filled.FormatListNumbered">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.StepsTitle"
|
||||
Label="عنوان بخش مراحل"
|
||||
Placeholder="سه گام تا شروع"
|
||||
Variant="Variant.Outlined" />
|
||||
@for (var i = 0; i < Model.Steps.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudAvatar Size="Size.Small" Color="Color.Primary">
|
||||
<MudText Typo="Typo.caption" Style="color:#fff;">@(index + 1)</MudText>
|
||||
</MudAvatar>
|
||||
<MudTextField @bind-Value="Model.Steps[index].Title"
|
||||
Label="عنوان مرحله"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="() => Model.Steps.RemoveAt(index)" />
|
||||
</MudStack>
|
||||
<MudTextField @bind-Value="Model.Steps[index].Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="2" />
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Text"
|
||||
OnClick="() => Model.Steps.Add(new StepModel())">
|
||||
افزودن مرحله
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Features ── *@
|
||||
<MudExpansionPanel Text="@($"ویژگیها ({Model.Features.Count})")" Icon="@Icons.Material.Filled.Star">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.FeaturesTitle"
|
||||
Label="عنوان بخش ویژگیها"
|
||||
Placeholder="چرا کارا بازار سلامت؟"
|
||||
Variant="Variant.Outlined" />
|
||||
@for (var i = 0; i < Model.Features.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
|
||||
<MudTextField @bind-Value="Model.Features[index].IconName"
|
||||
Label="نام آیکون"
|
||||
Placeholder="VerifiedUser"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudTextField @bind-Value="Model.Features[index].Title"
|
||||
Label="عنوان"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:2;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="() => Model.Features.RemoveAt(index)" />
|
||||
</MudStack>
|
||||
<MudTextField @bind-Value="Model.Features[index].Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="2" />
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Text"
|
||||
OnClick="() => Model.Features.Add(new FeatureModel())">
|
||||
افزودن ویژگی
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Stats ── *@
|
||||
<MudExpansionPanel Text="@($"آمار ({Model.Stats.Count})")" Icon="@Icons.Material.Filled.BarChart">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.StatsTitle"
|
||||
Label="عنوان بخش آمار"
|
||||
Placeholder="آمار پلتفرم"
|
||||
Variant="Variant.Outlined" />
|
||||
@for (var i = 0; i < Model.Stats.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
|
||||
<MudTextField @bind-Value="Model.Stats[index].Label"
|
||||
Label="عنوان"
|
||||
Placeholder="رشد میانگین تیم"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:2;" />
|
||||
<MudNumericField @bind-Value="Model.Stats[index].Value"
|
||||
Label="مقدار"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudTextField @bind-Value="Model.Stats[index].Suffix"
|
||||
Label="پسوند"
|
||||
Placeholder="%+"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudSelect @bind-Value="Model.Stats[index].Color"
|
||||
Label="رنگ"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;">
|
||||
<MudSelectItem Value="@("Primary")">آبی</MudSelectItem>
|
||||
<MudSelectItem Value="@("Secondary")">بنفش</MudSelectItem>
|
||||
<MudSelectItem Value="@("Success")">سبز</MudSelectItem>
|
||||
<MudSelectItem Value="@("Warning")">زرد</MudSelectItem>
|
||||
<MudSelectItem Value="@("Error")">قرمز</MudSelectItem>
|
||||
<MudSelectItem Value="@("Info")">آبی روشن</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="() => Model.Stats.RemoveAt(index)" />
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Text"
|
||||
OnClick="() => Model.Stats.Add(new StatModel())">
|
||||
افزودن آمار
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Testimonials ── *@
|
||||
<MudExpansionPanel Text="@($"نظرات مشتریان ({Model.Testimonials.Count})")" Icon="@Icons.Material.Filled.FormatQuote">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.TestimonialsTitle"
|
||||
Label="عنوان بخش نظرات"
|
||||
Placeholder="اعتماد مشتریان"
|
||||
Variant="Variant.Outlined" />
|
||||
@for (var i = 0; i < Model.Testimonials.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
|
||||
<MudTextField @bind-Value="Model.Testimonials[index].Name"
|
||||
Label="نام"
|
||||
Placeholder="شرکت سینا نت"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudTextField @bind-Value="Model.Testimonials[index].Role"
|
||||
Label="سمت"
|
||||
Placeholder="مدیر عملیات"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="() => Model.Testimonials.RemoveAt(index)" />
|
||||
</MudStack>
|
||||
<MudTextField @bind-Value="Model.Testimonials[index].Quote"
|
||||
Label="متن نظر"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="2" />
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Text"
|
||||
OnClick="() => Model.Testimonials.Add(new TestimonialModel())">
|
||||
افزودن نظر
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── FAQs ── *@
|
||||
<MudExpansionPanel Text="@($"سوالات متداول ({Model.Faqs.Count})")" Icon="@Icons.Material.Filled.QuestionAnswer">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.FaqTitle"
|
||||
Label="عنوان بخش سوالات"
|
||||
Placeholder="سوالات متداول"
|
||||
Variant="Variant.Outlined" />
|
||||
@for (var i = 0; i < Model.Faqs.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
|
||||
<MudTextField @bind-Value="Model.Faqs[index].Question"
|
||||
Label="سوال"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="flex:1;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="() => Model.Faqs.RemoveAt(index)" />
|
||||
</MudStack>
|
||||
<MudTextField @bind-Value="Model.Faqs[index].Answer"
|
||||
Label="پاسخ"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="2" />
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Text"
|
||||
OnClick="() => Model.Faqs.Add(new FaqModel())">
|
||||
افزودن سوال
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── CTA Section ── *@
|
||||
<MudExpansionPanel Text="بنر فراخوان (CTA)" Icon="@Icons.Material.Filled.Campaign">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.CtaTitle"
|
||||
Label="عنوان CTA"
|
||||
Placeholder="آماده شروع هستید؟"
|
||||
Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="Model.CtaDescription"
|
||||
Label="توضیحات CTA"
|
||||
Placeholder="همین الان ثبتنام کنید..."
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
<MudTextField @bind-Value="Model.CtaButtonText"
|
||||
Label="متن دکمه CTA"
|
||||
Placeholder="شروع رایگان"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Blog Section ── *@
|
||||
<MudExpansionPanel Text="بخش بلاگ" Icon="@Icons.Material.Filled.Article">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="_featuredBlogEnabled" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">نمایش آخرین مطالب بلاگ</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
</MudExpansionPanels>
|
||||
|
||||
@code {
|
||||
[Parameter] public LandingSettingsModel Model { get; set; } = new();
|
||||
|
||||
private bool _featuredBlogEnabled
|
||||
{
|
||||
get => Model.FeaturedBlogEnabled ?? true;
|
||||
set => Model.FeaturedBlogEnabled = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@using BackOffice.Pages.Content.Components.SettingsModels
|
||||
|
||||
<MudExpansionPanels MultiExpansion="true" Elevation="0">
|
||||
|
||||
@* ── Page Settings ── *@
|
||||
<MudExpansionPanel Text="تنظیمات صفحه" Icon="@Icons.Material.Filled.Settings" IsInitiallyExpanded="true">
|
||||
<MudStack Spacing="2" Class="pa-2">
|
||||
<MudTextField @bind-Value="Model.PageDescription"
|
||||
Label="توضیحات صفحه"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
Placeholder="مجوزها و نمادهای اعتماد الکترونیکی ما..." />
|
||||
<MudSelect @bind-Value="Model.DisplayStyle"
|
||||
Label="نوع نمایش"
|
||||
Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="@("grid")">گرید (Grid)</MudSelectItem>
|
||||
<MudSelectItem Value="@("list")">لیست (List)</MudSelectItem>
|
||||
<MudSelectItem Value="@("carousel")">اسلایدر (Carousel)</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@* ── Hint ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mt-2">
|
||||
تصاویر مجوزها از بخش «مدیریت تصاویر» قابل افزودن و ویرایش هستند.
|
||||
</MudAlert>
|
||||
|
||||
</MudExpansionPanels>
|
||||
|
||||
@code {
|
||||
[Parameter] public LicensesSettingsModel Model { get; set; } = new();
|
||||
}
|
||||
@@ -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,111 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using BackOffice.Pages.Content.Components.SettingsModels
|
||||
@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.subtitle1" Class="mb-1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Tune" Size="Size.Small" Class="ml-1" />
|
||||
تنظیمات اختصاصی «@PageTitle»
|
||||
</MudText>
|
||||
|
||||
@* ── Typed Settings Editors ── *@
|
||||
@switch (Model.PageKey)
|
||||
{
|
||||
case "landing":
|
||||
<LandingSettingsEditor Model="_landingSettings" />
|
||||
break;
|
||||
case "about":
|
||||
<AboutSettingsEditor Model="_aboutSettings" />
|
||||
break;
|
||||
case "contact":
|
||||
<ContactSettingsEditor Model="_contactSettings" />
|
||||
break;
|
||||
case "licenses":
|
||||
<LicensesSettingsEditor Model="_licensesSettings" />
|
||||
break;
|
||||
default:
|
||||
<MudTextField @bind-Value="Model.SettingsJson"
|
||||
Label="تنظیمات JSON"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="8"
|
||||
HelperText="تنظیمات اختصاصی صفحه (JSON)" />
|
||||
break;
|
||||
}
|
||||
|
||||
<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,151 @@
|
||||
using System.Text.Json;
|
||||
using BackOffice.Pages.Content.Components.SettingsModels;
|
||||
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;
|
||||
|
||||
// ── Typed settings models ──
|
||||
private LandingSettingsModel _landingSettings = new();
|
||||
private AboutSettingsModel _aboutSettings = new();
|
||||
private ContactSettingsModel _contactSettings = new();
|
||||
private LicensesSettingsModel _licensesSettings = new();
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
DeserializeSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse existing SettingsJson into the typed model based on PageKey
|
||||
/// </summary>
|
||||
private void DeserializeSettings()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Model.SettingsJson)) return;
|
||||
|
||||
try
|
||||
{
|
||||
switch (Model.PageKey)
|
||||
{
|
||||
case "landing":
|
||||
_landingSettings = JsonSerializer.Deserialize<LandingSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
|
||||
break;
|
||||
case "about":
|
||||
_aboutSettings = JsonSerializer.Deserialize<AboutSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
|
||||
break;
|
||||
case "contact":
|
||||
_contactSettings = JsonSerializer.Deserialize<ContactSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
|
||||
break;
|
||||
case "licenses":
|
||||
_licensesSettings = JsonSerializer.Deserialize<LicensesSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If JSON is malformed, start with empty model
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the typed model back to JSON before saving
|
||||
/// </summary>
|
||||
private void SerializeSettings()
|
||||
{
|
||||
Model.SettingsJson = Model.PageKey switch
|
||||
{
|
||||
"landing" => JsonSerializer.Serialize(_landingSettings, JsonOptions),
|
||||
"about" => JsonSerializer.Serialize(_aboutSettings, JsonOptions),
|
||||
"contact" => JsonSerializer.Serialize(_contactSettings, JsonOptions),
|
||||
"licenses" => JsonSerializer.Serialize(_licensesSettings, JsonOptions),
|
||||
_ => Model.SettingsJson // keep raw JSON for unknown pages
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Serialize typed model → JSON before saving
|
||||
SerializeSettings();
|
||||
|
||||
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,108 @@
|
||||
namespace BackOffice.Pages.Content.Components.SettingsModels;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// LANDING PAGE
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
public class LandingSettingsModel
|
||||
{
|
||||
public string? HeroButtonPrimaryText { get; set; }
|
||||
public string? HeroButtonSecondaryText { get; set; }
|
||||
public List<TrustBadgeModel> TrustBadges { get; set; } = new();
|
||||
public string? StepsTitle { get; set; }
|
||||
public List<StepModel> Steps { get; set; } = new();
|
||||
public string? FeaturesTitle { get; set; }
|
||||
public List<FeatureModel> Features { get; set; } = new();
|
||||
public string? StatsTitle { get; set; }
|
||||
public List<StatModel> Stats { get; set; } = new();
|
||||
public string? TestimonialsTitle { get; set; }
|
||||
public List<TestimonialModel> Testimonials { get; set; } = new();
|
||||
public string? FaqTitle { get; set; }
|
||||
public List<FaqModel> Faqs { get; set; } = new();
|
||||
public string? CtaTitle { get; set; }
|
||||
public string? CtaDescription { get; set; }
|
||||
public string? CtaButtonText { get; set; }
|
||||
public bool? FeaturedBlogEnabled { get; set; }
|
||||
}
|
||||
|
||||
public class TrustBadgeModel
|
||||
{
|
||||
public string? IconName { get; set; }
|
||||
public string? Text { get; set; }
|
||||
}
|
||||
|
||||
public class StepModel
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class FeatureModel
|
||||
{
|
||||
public string? IconName { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class StatModel
|
||||
{
|
||||
public string? Label { get; set; }
|
||||
public double Value { get; set; }
|
||||
public string? Suffix { get; set; }
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
||||
public class TestimonialModel
|
||||
{
|
||||
public string? Quote { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Role { get; set; }
|
||||
}
|
||||
|
||||
public class FaqModel
|
||||
{
|
||||
public string? Question { get; set; }
|
||||
public string? Answer { get; set; }
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// ABOUT PAGE
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
public class AboutSettingsModel
|
||||
{
|
||||
public string? VisionTitle { get; set; }
|
||||
public string? VisionText { get; set; }
|
||||
public string? VisionIcon { get; set; }
|
||||
public string? MissionTitle { get; set; }
|
||||
public string? MissionText { get; set; }
|
||||
public string? MissionIcon { get; set; }
|
||||
public string? ValuesTitle { get; set; }
|
||||
public string? TeamTitle { get; set; }
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CONTACT PAGE
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
public class ContactSettingsModel
|
||||
{
|
||||
public string? Address { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? WorkingHours { get; set; }
|
||||
public string? TelegramUrl { get; set; }
|
||||
public string? InstagramUrl { get; set; }
|
||||
public string? LinkedinUrl { get; set; }
|
||||
public string? WhatsappUrl { get; set; }
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// LICENSES PAGE
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
public class LicensesSettingsModel
|
||||
{
|
||||
public string? PageDescription { get; set; }
|
||||
public string? DisplayStyle { get; set; }
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using InventoryProductType = CMSMicroservice.Protobuf.Protos.Inventory.ProductType
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -19,12 +20,16 @@
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudNumericField T="long" @bind-Value="_productId"
|
||||
Label="شناسه محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="شناسه محصول الزامی است"
|
||||
Min="1" />
|
||||
@if (_productType == InventoryProductType.RegularProduct)
|
||||
{
|
||||
<ProductsAutoComplete @bind-Value="_productId"
|
||||
Label="انتخاب محصول عادی" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<DiscountProductsAutoComplete @bind-Value="_discountProductId"
|
||||
Label="انتخاب محصول تخفیفی" />
|
||||
}
|
||||
</MudItem>
|
||||
}
|
||||
else
|
||||
@@ -103,7 +108,8 @@
|
||||
private bool _isSubmitting;
|
||||
|
||||
private InventoryProductType _productType = InventoryProductType.RegularProduct;
|
||||
private long _productId;
|
||||
private long? _productId;
|
||||
private long? _discountProductId;
|
||||
private long? _warehouseId;
|
||||
private int _quantity = 1;
|
||||
private string _referenceNumber = string.Empty;
|
||||
@@ -111,9 +117,12 @@
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (ProductIdParam.HasValue)
|
||||
if (ProductIdParam.HasValue && ProductTypeParam.HasValue)
|
||||
{
|
||||
_productId = ProductIdParam.Value;
|
||||
if (ProductTypeParam.Value == InventoryProductType.DiscountProduct)
|
||||
_discountProductId = ProductIdParam.Value;
|
||||
else
|
||||
_productId = ProductIdParam.Value;
|
||||
}
|
||||
if (ProductTypeParam.HasValue)
|
||||
{
|
||||
@@ -144,9 +153,19 @@
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var selectedProductId = ProductIdParam
|
||||
?? (_productType == InventoryProductType.DiscountProduct ? _discountProductId : _productId);
|
||||
|
||||
if (!selectedProductId.HasValue || selectedProductId.Value <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفاً یک محصول انتخاب کنید", Severity.Warning);
|
||||
_isSubmitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new AddStockRequest
|
||||
{
|
||||
ProductId = ProductIdParam ?? _productId,
|
||||
ProductId = selectedProductId.Value,
|
||||
ProductType = ProductTypeParam ?? _productType,
|
||||
Quantity = _quantity,
|
||||
ReferenceNumber = _referenceNumber ?? string.Empty,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"GwUrl": "http://localhost:32847",
|
||||
// "GwUrl": "https://localhost:32846",
|
||||
// "GwUrl": "https://backoffice-bff.se.kbs1.ir", // OLD - via BFF
|
||||
// "GwUrl": "https://cms.se.kbs1.ir", // NEW - direct to CMS
|
||||
"GwUrl": "http://localhost:32847",
|
||||
"Authentication": {
|
||||
"Authority": "https://ids.afrino.co/",
|
||||
"ClientId": "client_backoffice_spa"
|
||||
|
||||
Reference in New Issue
Block a user