u
This commit is contained in:
@@ -166,7 +166,8 @@
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true">مشاهده جزئیات</MudButton>
|
||||
FullWidth="true"
|
||||
OnClick="@(() => NavigateToPackage(p.Id))">مشاهده جزئیات</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -28,6 +26,7 @@ public partial class Index
|
||||
if (response?.Models?.Any() == true)
|
||||
{
|
||||
_packs = response.Models.Select(p => new Pack(
|
||||
p.Id,
|
||||
p.Title,
|
||||
p.Description,
|
||||
Image: UrlUtility.DownloadUrl + p.ImagePath
|
||||
@@ -60,16 +59,14 @@ public partial class Index
|
||||
_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 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()
|
||||
{
|
||||
new("دامنهٔ اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر میشود."),
|
||||
|
||||
206
src/FrontOffice.Main/Pages/PackageDetail.razor
Normal file
206
src/FrontOffice.Main/Pages/PackageDetail.razor
Normal 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>
|
||||
}
|
||||
}
|
||||
214
src/FrontOffice.Main/Pages/PackageDetail.razor.cs
Normal file
214
src/FrontOffice.Main/Pages/PackageDetail.razor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,9 @@ public static class RouteConstants
|
||||
{
|
||||
public const string Index = "/profile";
|
||||
}
|
||||
|
||||
public static class Package
|
||||
{
|
||||
public const string Detail = "/package/";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user