Add MgCardView component & refactor MeasuringOut to tabs
Introduced a reusable, responsive MgCardView<TItem> component for displaying data as cards with optional filtering and paging. Refactored the MeasuringOut page to use a tabbed interface: daily tasks are now shown as filterable cards, and measuring details are separated into a dedicated tab. Improved UI clarity, code organization, and maintainability.
This commit is contained in:
parent
0da7b67c60
commit
40223f9182
|
|
@ -0,0 +1,31 @@
|
||||||
|
@typeparam TItem
|
||||||
|
|
||||||
|
@if (ShowFilterPanel && FilterPanel is not null)
|
||||||
|
{
|
||||||
|
<div class="mg-card-filter-panel">
|
||||||
|
@FilterPanel
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Data is { Count: > 0 })
|
||||||
|
{
|
||||||
|
<div class="mg-card-grid @CssClass"
|
||||||
|
style="--cols-xs: @ColumnCountXs; --cols-sm: @ColumnCountSm; --cols-lg: @ColumnCountLg;">
|
||||||
|
@foreach (var item in PagedItems)
|
||||||
|
{
|
||||||
|
<div class="mg-card @CardCssClass"
|
||||||
|
@onclick="() => OnCardClickInternal(item)"
|
||||||
|
style="@(OnCardClick.HasDelegate ? "cursor: pointer;" : "")">
|
||||||
|
@CardTemplate(item)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (ShowPager && Data.Count > PageSize)
|
||||||
|
{
|
||||||
|
<DxPager PageCount="@((int)Math.Ceiling((double)Data.Count / PageSize))"
|
||||||
|
ActivePageIndex="_activePageIndex"
|
||||||
|
ActivePageIndexChanged="OnActivePageIndexChanged"
|
||||||
|
CssClass="mt-2" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AyCode.Blazor.Components.Components.CardViews;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic card view component that displays items in a responsive grid layout.
|
||||||
|
/// Uses DxGridLayout + DxLayoutBreakpoint for responsive column management
|
||||||
|
/// and DxPager for optional pagination.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TItem">The type of data item displayed in each card.</typeparam>
|
||||||
|
public partial class MgCardView<TItem> : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The collection of items to display as cards.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public IReadOnlyList<TItem> Data { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Template for rendering each card's content.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public RenderFragment<TItem> CardTemplate { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when a card is clicked/tapped.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<TItem> OnCardClick { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of columns on extra-small screens (below 576px). Default: 1.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public int ColumnCountXs { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of columns on small screens (576–768px). Default: 2.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public int ColumnCountSm { get; set; } = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of columns on medium+ screens (769px+). Default: 3.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public int ColumnCountLg { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to show the pager below the cards. Default: false.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowPager { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items per page when paging is enabled. Default: 12.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public int PageSize { get; set; } = 12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional CSS class for the card view container.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string? CssClass { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional CSS class applied to each individual card wrapper.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string? CardCssClass { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to show the filter panel above the cards. Default: false.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowFilterPanel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom content for the filter panel. Rendered above the card grid when ShowFilterPanel is true.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment? FilterPanel { get; set; }
|
||||||
|
|
||||||
|
private int _activePageIndex;
|
||||||
|
|
||||||
|
private IReadOnlyList<TItem> PagedItems
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!ShowPager)
|
||||||
|
return Data;
|
||||||
|
|
||||||
|
return Data
|
||||||
|
.Skip(_activePageIndex * PageSize)
|
||||||
|
.Take(PageSize)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnCardClickInternal(TItem item)
|
||||||
|
{
|
||||||
|
if (OnCardClick.HasDelegate)
|
||||||
|
await OnCardClick.InvokeAsync(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnActivePageIndexChanged(int newPageIndex)
|
||||||
|
{
|
||||||
|
_activePageIndex = newPageIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
.mg-card-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(var(--cols-xs, 1), 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.mg-card-grid {
|
||||||
|
grid-template-columns: repeat(var(--cols-sm, 2), 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mg-card-grid {
|
||||||
|
grid-template-columns: repeat(var(--cols-lg, 3), 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mg-card {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.15s ease;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mg-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mg-card-filter-panel {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue