This commit is contained in:
MeysamMoghaddam
2025-10-09 21:55:18 +03:30
parent 585445d338
commit 2c8b3c8483
5 changed files with 434 additions and 11 deletions

View File

@@ -166,7 +166,8 @@
<MudCardActions> <MudCardActions>
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined"
Color="Color.Primary" Color="Color.Primary"
FullWidth="true">مشاهده جزئیات</MudButton> FullWidth="true"
OnClick="@(() => NavigateToPackage(p.Id))">مشاهده جزئیات</MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
</MudItem> </MudItem>

View File

@@ -1,7 +1,5 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package; using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.Main.Utilities; using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
@@ -28,6 +26,7 @@ public partial class Index
if (response?.Models?.Any() == true) if (response?.Models?.Any() == true)
{ {
_packs = response.Models.Select(p => new Pack( _packs = response.Models.Select(p => new Pack(
p.Id,
p.Title, p.Title,
p.Description, p.Description,
Image: UrlUtility.DownloadUrl + p.ImagePath Image: UrlUtility.DownloadUrl + p.ImagePath
@@ -60,16 +59,14 @@ public partial class Index
_email = string.Empty; _email = string.Empty;
} }
private record Pack(string Title, string Body, string Image); private void NavigateToPackage(long packageId)
{
Navigation.NavigateTo($"{RouteConstants.Package.Detail}/{packageId}");
}
private record Pack(long Id,string Title, string Body, string Image);
private record Plan(string Name, string Price, bool Highlight, IEnumerable<string> Features); private record Plan(string Name, string Price, bool Highlight, IEnumerable<string> Features);
private record QA(string Q, string A); private record QA(string Q, string A);
private readonly List<Plan> _plans = new()
{
new("استارتر", "رایگان", false, new []{ "تا ۲۰۰ عضو", "شجره‌نامه پایه", "پشتیبانی ایمیلی" }),
new("رشد", "۳۹ دلار / ماه", true, new []{ "تا ۵۰۰۰ عضو", "شجره‌نامه پیشرفته", "موتور کارمزد", "پشتیبانی اولویت‌دار" }),
new("اسکیل", "تماس بگیرید", false, new []{ "نامحدود", "قوانین سفارشی", "SLA و آن‌بوردینگ", "مدیر موفقیت اختصاصی" }),
};
private readonly List<QA> _faqs = new() private readonly List<QA> _faqs = new()
{ {
new("دامنهٔ اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."), new("دامنهٔ اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."),

View File

@@ -0,0 +1,206 @@
@attribute [Route(RouteConstants.Package.Detail + "{id:long}")]
<PageTitle>@(_package?.Title ?? "جزئیات پکیج")</PageTitle>
@if (_isLoading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_package == null)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudIcon Icon="@Icons.Material.Filled.Error" Size="Size.Large" Color="Color.Error" />
<MudText Typo="Typo.h5" Class="mt-2">پکیج یافت نشد</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">پکیج مورد نظر وجود ندارد یا حذف شده است.</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4" OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
بازگشت به صفحه اصلی
</MudButton>
</MudStack>
}
else
{
<!-- Breadcrumb -->
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4">
<MudBreadcrumbs Items="_breadcrumbItems" />
</MudContainer>
<!-- Package Details -->
<MudContainer MaxWidth="MaxWidth.Large" Class="pb-8">
<MudGrid Spacing="4">
<!-- Main Content -->
<MudItem xs="12" lg="8">
<MudPaper Elevation="2" Class="pa-6 rounded-2xl mud-theme-surface">
<!-- Package Header -->
<MudStack Spacing="4">
<!-- Package Description -->
<div>
<MudText Typo="Typo.h6" Class="mb-3 mud-typography-subtitle1">توضیحات پکیج</MudText>
@((MarkupString)_package.Body)
</div>
<!-- Package Specifications -->
<div>
<MudText Typo="Typo.h6" Class="mb-3 mud-typography-subtitle1">مشخصات پکیج</MudText>
<MudGrid Spacing="2">
@foreach (var spec in _package.Specifications)
{
<MudItem xs="12" sm="6">
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@spec.Icon" Size="Size.Small" Color="Color.Primary" />
<div>
<MudText Typo="Typo.body2" Class="fw-600">@spec.Name</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@spec.Value</MudText>
</div>
</MudStack>
</MudPaper>
</MudItem>
}
</MudGrid>
</div>
</MudStack>
</MudPaper>
<!-- Reviews Section -->
<MudPaper Elevation="2" Class="pa-6 rounded-2xl mud-theme-surface mt-4">
<MudText Typo="Typo.h6" Class="mb-4 mud-typography-subtitle1">نظرات کاربران</MudText>
@if (_reviews.Any())
{
<MudStack Spacing="3">
@foreach (var review in _reviews)
{
<MudPaper Outlined="true" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" />
</MudAvatar>
<div>
<MudText Typo="Typo.body2" Class="fw-600">@(review.UserName)</MudText>
<MudRating ReadOnly="true" Value="review.Rating" Size="Size.Small" />
</div>
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(review.Date)</MudText>
</MudStack>
<MudText Typo="Typo.body2">@(review.Comment)</MudText>
</MudStack>
</MudPaper>
}
</MudStack>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudIcon Icon="@Icons.Material.Filled.Chat" Size="Size.Large" Color="Color.Default" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">هنوز نظری ثبت نشده است.</MudText>
</MudStack>
}
</MudPaper>
</MudItem>
<!-- Sidebar -->
<MudItem xs="12" lg="4">
<MudPaper Elevation="2" Class="pa-6 rounded-2xl mud-theme-surface sticky-top">
<MudStack Class="mb-4">
<!-- Package Image -->
<MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="@_package.Image"
Alt="@_package.Title"
Height="300"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
<MudText Typo="Typo.h4" Class="px-2">@(_package.Title)</MudText>
</MudStack>
<!-- Pricing -->
<MudStack Spacing="3">
@if (_package.Pricing.HasDiscount)
{
<div>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5" Class="text-decoration-line-through mud-text-secondary">@_package.Pricing.OriginalPrice.ToString("N0") تومان</MudText>
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">@_package.Pricing.DiscountPercent% تخفیف</MudChip>
</MudStack>
<MudText Typo="Typo.h4" Color="Color.Success" Class="fw-700">@_package.Pricing.FinalPrice.ToString("N0") تومان</MudText>
</div>
}
else
{
<MudText Typo="Typo.h4" Color="Color.Primary" Class="fw-700">@_package.Pricing.FinalPrice.ToString("N0") تومان</MudText>
}
<!-- Purchase Button -->
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Size="Size.Large"
OnClick="PurchasePackage"
Disabled="_isPurchasing">
@(_isPurchasing ? "در حال پردازش..." : "خرید پکیج")
</MudButton>
<!-- Package Features -->
<div>
<MudText Typo="Typo.subtitle2" Class="mb-2 fw-600">شامل:</MudText>
<MudStack Spacing="1">
@foreach (var feature in _package.Features)
{
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Start">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Success" Class="mt-1" />
<MudText Typo="Typo.body2">@feature</MudText>
</MudStack>
}
</MudStack>
</div>
<!-- Support Info -->
<MudDivider Class="my-2" />
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Support" Size="Size.Small" Color="Color.Info" />
<MudText Typo="Typo.body2">پشتیبانی ۲۴ ساعته</MudText>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
<!-- Related Packages -->
@if (_relatedPackages.Any())
{
<section class="py-8 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h5" Align="Align.Center" Class="mb-6 mud-typography-subtitle1">پکیج‌های پیشنهادی</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var relatedPackage in _relatedPackages)
{
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-2xl d-flex flex-column h-100 cursor-pointer"
OnClick="() => NavigateToPackage(relatedPackage.Id)">
<MudImage Src="@relatedPackage.Image"
Alt="@relatedPackage.Title"
Height="200"
ObjectFit="ObjectFit.Cover"
Class="rounded-xl mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">@relatedPackage.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3 line-clamp-2">@relatedPackage.ShortDescription</MudText>
<MudSpacer />
<MudText Typo="Typo.h6" Color="Color.Primary">@relatedPackage.Pricing.FinalPrice.ToString("N0") تومان</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
}
}

View File

@@ -0,0 +1,214 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.Main.Utilities;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages;
public partial class PackageDetail : IDisposable
{
[Parameter] public long Id { get; set; }
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
private PackageDetailDto? _package;
private List<Review> _reviews = new();
private List<RelatedPackage> _relatedPackages = new();
private bool _isLoading = true;
private bool _isPurchasing;
private CancellationTokenSource? _loadCts;
private List<BreadcrumbItem> _breadcrumbItems = new()
{
new BreadcrumbItem("صفحه اصلی", RouteConstants.Main.MainPage),
new BreadcrumbItem("پکیج‌ها", "#features"),
new BreadcrumbItem("جزئیات پکیج", null, disabled: true)
};
protected override async Task OnInitializedAsync()
{
if (Id < 1)
{
_isLoading = false;
return;
}
await LoadPackageDetailsAsync();
}
protected override async Task OnParametersSetAsync()
{
if (Id > 0)
{
await LoadPackageDetailsAsync();
}
}
private async Task LoadPackageDetailsAsync()
{
_isLoading = true;
_loadCts?.Cancel();
_loadCts?.Dispose();
_loadCts = new CancellationTokenSource();
try
{
// Load package details
var packageRequest = new GetPackageRequest { Id = Id };
var packageResponse = await PackageClient.GetPackageAsync(request: new() { Id = Id}, cancellationToken: _loadCts.Token);
if (packageResponse != null)
{
_package = new PackageDetailDto
{
Id = packageResponse.Id,
Title = packageResponse.Title,
Body = packageResponse.Description,
Image = UrlUtility.DownloadUrl + packageResponse.ImagePath,
Specifications = new List<Specification>
{
new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group },
new() { Name = "شجره‌نامه", Value = "پیشرفته", Icon = Icons.Material.Filled.AccountTree },
new() { Name = "گزارش‌گیری", Value = "جامع", Icon = Icons.Material.Filled.Analytics },
new() { Name = "پشتیبانی", Value = "۲۴ ساعته", Icon = Icons.Material.Filled.Support }
},
Features = new List<string>
{
"مدیریت تیم نامحدود",
"شجره‌نامه بصری",
"محاسبه کارمزد خودکار",
"گزارش‌های مالی",
"پشتیبانی اولویت‌دار"
},
Pricing = new PricingInfo
{
OriginalPrice = packageResponse.Price,
FinalPrice = packageResponse.Price,
HasDiscount = false,
DiscountPercent = 0
}
};
// Load reviews (mock data for now)
await LoadReviewsAsync();
// Load related packages
await LoadRelatedPackagesAsync();
}
}
catch (RpcException rpcEx)
{
Snackbar.Add($"خطا در بارگذاری پکیج: {rpcEx.Status.Detail}", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری پکیج: {ex.Message}", Severity.Error);
}
finally
{
_isLoading = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task LoadReviewsAsync()
{
// TODO: Load reviews from API
_reviews = new List<Review>
{
new() { UserName = "علی احمدی", Rating = 5, Comment = "عالی! کارمزد رو دقیق حساب می‌کنه و گزارش‌ها کامل هستن.", Date = "۱۴۰۲/۱۰/۰۵" },
new() { UserName = "مریم رضایی", Rating = 4, Comment = "رابط کاربری خوبی داره، فقط سرعت بارگذاری می‌تونه بهتر بشه.", Date = "۱۴۰۲/۰۹/۲۲" },
new() { UserName = "حسن کریمی", Rating = 5, Comment = "پشتیبانی فوق‌العاده سریع و حرفه‌ای داشتن. پیشنهاد می‌کنم.", Date = "۱۴۰۲/۰۹/۱۵" }
};
}
private async Task LoadRelatedPackagesAsync()
{
// TODO: Load related packages from API
_relatedPackages = new List<RelatedPackage>
{
new() { Id = "2", Title = "پکیج رشد", ShortDescription = "مناسب برای تیم‌های در حال توسعه", Image = "https://images.unsplash.com/photo-1552664730-d307ca884978?q=80&w=400", Pricing = new PricingInfo { FinalPrice = 750000 } },
new() { Id = "3", Title = "پکیج حرفه‌ای", ShortDescription = "برای کسب‌وکارهای بزرگ", Image = "https://images.unsplash.com/photo-1460925895917-afdab827c52f?q=80&w=400", Pricing = new PricingInfo { FinalPrice = 1200000 } }
};
}
private async Task PurchasePackage()
{
if (_package == null) return;
_isPurchasing = true;
try
{
// TODO: Implement purchase logic
await Task.Delay(2000); // Simulate API call
Snackbar.Add("پکیج با موفقیت خریداری شد!", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در خرید پکیج: {ex.Message}", Severity.Error);
}
finally
{
_isPurchasing = false;
await InvokeAsync(StateHasChanged);
}
}
private void NavigateToPackage(string packageId)
{
Navigation.NavigateTo($"{RouteConstants.Package.Detail}/{packageId}");
}
public void Dispose()
{
_loadCts?.Cancel();
_loadCts?.Dispose();
_loadCts = null;
}
public class PackageDetailDto
{
public long? Id { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public string? Image { get; set; }
public List<Specification> Specifications { get; set; } = new();
public List<string> Features { get; set; } = new();
public PricingInfo Pricing { get; set; } = new();
}
public class Specification
{
public string? Name { get; set; }
public string? Value { get; set; }
public string? Icon { get; set; }
}
public class PricingInfo
{
public long OriginalPrice { get; set; }
public long FinalPrice { get; set; }
public bool HasDiscount { get; set; }
public int DiscountPercent { get; set; }
}
public class Review
{
public string? UserName { get; set; }
public int Rating { get; set; }
public string? Comment { get; set; }
public string? Date { get; set; }
}
public class RelatedPackage
{
public string? Id { get; set; }
public string? Title { get; set; }
public string? ShortDescription { get; set; }
public string? Image { get; set; }
public PricingInfo Pricing { get; set; } = new();
}
}

View File

@@ -17,4 +17,9 @@ public static class RouteConstants
{ {
public const string Index = "/profile"; public const string Index = "/profile";
} }
public static class Package
{
public const string Detail = "/package/";
}
} }