mirror of
https://gitee.com/LongbowEnterprise/BootstrapBlazor.git
synced 2024-12-05 13:39:39 +08:00
!3537 refactor(#I61GRJ): redesign search box in table reuse card component
* chore: 更新单元测试 * test: 更新单元测试 * doc: 更新示例 * feat: 更新 Search 键盘响应移除 js 代码 * refactor: 更新 Table 搜索组件复用 Card 组件 * fix: 更新 card-header 样式增加 flex: 1
This commit is contained in:
parent
60d41b8a75
commit
2b0621f14f
@ -78,7 +78,7 @@
|
||||
<TableColumn @bind-Field="@context.DateTime" Width="180" Filterable="true" />
|
||||
<TableColumn @bind-Field="@context.Name" Searchable="true" />
|
||||
<TableColumn @bind-Field="@context.Address" Searchable="true" />
|
||||
<TableColumn @bind-Field="@context.Education" Searchable="true" />
|
||||
<TableColumn @bind-Field="@context.Education" />
|
||||
</TableColumns>
|
||||
</Table>
|
||||
</DemoBlock>
|
||||
|
@ -113,12 +113,16 @@ public sealed partial class TablesSearch
|
||||
// 设置记录总数
|
||||
var total = items.Count();
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchModel.Name))
|
||||
if (!string.IsNullOrEmpty(options.SearchText))
|
||||
{
|
||||
items = items.Where(i => (i.Name?.Contains(options.SearchText, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
|| (i.Address?.Contains(options.SearchText, StringComparison.OrdinalIgnoreCase) ?? false));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(SearchModel.Name))
|
||||
{
|
||||
items = items.Where(i => i.Name == SearchModel.Name);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchModel.Address))
|
||||
else if (!string.IsNullOrEmpty(SearchModel.Address))
|
||||
{
|
||||
items = items.Where(i => i.Address == SearchModel.Address);
|
||||
}
|
||||
|
@ -21,12 +21,17 @@
|
||||
.card-header .card-collapse-bar {
|
||||
cursor: pointer;
|
||||
margin-right: var(--bb-card-collapse-bar-margin-right);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-header .tag {
|
||||
line-height: var(--bb-card-header-tag-height);
|
||||
}
|
||||
|
||||
.card-header + .card-body {
|
||||
border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--bb-card-shadow);
|
||||
transition: box-shadow .3s linear;
|
||||
@ -40,10 +45,6 @@
|
||||
transform: rotate( 90deg);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);
|
||||
}
|
||||
|
||||
.is-collapsable > .card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
--bb-table-header-icon-active-color: #409eff;
|
||||
--bb-table-header-icon-hover-bg: #ddd;
|
||||
--bb-table-header-icon-hover-color: #606266;
|
||||
--bb-table-header-min-height: 37px;
|
||||
--bb-table-footer-font-weight: blod;
|
||||
--bb-table-footer-border-top: 2px solid #dee2e6;
|
||||
--bb-table-loader-bg: #f8f9fa;
|
||||
@ -603,63 +604,28 @@ form .table .table-cell > textarea {
|
||||
}
|
||||
|
||||
.table-search .card-header {
|
||||
padding: 0;
|
||||
--bs-card-cap-padding-y: 0;
|
||||
min-height: var(--bb-table-header-min-height);
|
||||
}
|
||||
|
||||
.table-search .card-header .table-search-title {
|
||||
padding: .5rem 1rem;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
.table-search .card-header .table-search-buttons .btn {
|
||||
padding: .275rem .5rem;
|
||||
}
|
||||
|
||||
.table-search .card-header .table-search-buttons {
|
||||
padding: .25rem 1rem;
|
||||
.table-search .card-header .input-group {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.table-search .card-header .table-search-buttons .btn {
|
||||
padding: .275rem .5rem;
|
||||
.table-search .card-header .input-group .table-toolbar-search,
|
||||
.table-search .card-header .input-group .btn {
|
||||
height: calc(var(--bb-table-header-min-height) - 4px);
|
||||
}
|
||||
|
||||
.table-search-collapse {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-search-collapse .table-search-collapse-arrow {
|
||||
cursor: pointer;
|
||||
transition: .3s transform linear;
|
||||
transform: rotate(0);
|
||||
.table-search .card-header [aria-expanded="true"] + .input-group,
|
||||
.table-search .card-header [aria-expanded="false"] + .input-group + .table-search-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-search-collapse[aria-expanded="true"] .table-search-collapse-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.table-search-collapse .table-search-collapse-title {
|
||||
cursor: pointer;
|
||||
margin-left: .5rem;
|
||||
flex: 1;
|
||||
padding: .5rem 0;
|
||||
}
|
||||
|
||||
.table-search .card-header .input-group {
|
||||
width: auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.table-search .card-header .input-group .table-toolbar-search,
|
||||
.table-search .card-header .input-group .btn {
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
.table-search .card-header [aria-expanded="true"] + .input-group,
|
||||
.table-search .card-header [aria-expanded="false"] + .input-group + .table-search-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-excel-toolbar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
@ -123,7 +123,7 @@
|
||||
@if (ShowSearchTextTooltip)
|
||||
{
|
||||
<Tooltip Placement="Placement.Top" Title="@SearchTooltip" Sanitize="false" IsHtml="true">
|
||||
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText" ShowLabel="false" />
|
||||
<BootstrapInput class="table-toolbar-search" @onkeyup="OnSearchKeyup" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText" ShowLabel="false" />
|
||||
</Tooltip>
|
||||
}
|
||||
else
|
||||
@ -834,75 +834,67 @@ RenderFragment<TItem> RenderRowExtendButtons => item =>
|
||||
</td>;
|
||||
|
||||
RenderFragment RenderSearch =>
|
||||
@<div class="card is-collapsable">
|
||||
<div class="card-header">
|
||||
<div class="d-flex">
|
||||
<div class="table-search-collapse" data-bs-toggle="collapse" data-bs-target="#@TopSearchBodyId" aria-expanded="@TopSearchCollapsedString">
|
||||
<i class="table-search-collapse-arrow fa-solid fa-circle-chevron-right"></i>
|
||||
<div class="table-search-collapse-title">@SearchModalTitle</div>
|
||||
</div>
|
||||
@if (ShowSearchText)
|
||||
{
|
||||
<div class="input-group">
|
||||
@if (ShowSearchTextTooltip)
|
||||
{
|
||||
<Tooltip Placement="Placement.Top" Title="@SearchTooltip" Sanitize="false" IsHtml="true">
|
||||
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText">
|
||||
</BootstrapInput>
|
||||
</Tooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText">
|
||||
@<Card IsCollapsible="true" HeaderText="@SearchModalTitle">
|
||||
<HeaderTemplate>
|
||||
@if (ShowSearchText)
|
||||
{
|
||||
<div class="input-group">
|
||||
@if (ShowSearchTextTooltip)
|
||||
{
|
||||
<Tooltip Placement="Placement.Top" Title="@SearchTooltip" Sanitize="false" IsHtml="true">
|
||||
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @onkeyup="OnSearchKeyup" @bind-Value="@SearchText">
|
||||
</BootstrapInput>
|
||||
}
|
||||
@if (ShowResetButton)
|
||||
{
|
||||
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" OnClickWithoutRender="ClearSearchClick">
|
||||
<span class="d-none d-sm-inline-block">@ResetSearchButtonText</span>
|
||||
</Button>
|
||||
}
|
||||
<Button Color="Color.Secondary" Icon="fa-solid fa-magnifying-glass" OnClickWithoutRender="SearchClick">
|
||||
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
<div class="table-search-buttons">
|
||||
</Tooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @onkeyup="OnSearchKeyup" @bind-Value="@SearchText" >
|
||||
</BootstrapInput>
|
||||
}
|
||||
@if (ShowResetButton)
|
||||
{
|
||||
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" class="btn-xs ms-2" OnClickWithoutRender="ClearSearchClick">
|
||||
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" OnClickWithoutRender="ClearSearchClick">
|
||||
<span class="d-none d-sm-inline-block">@ResetSearchButtonText</span>
|
||||
</Button>
|
||||
}
|
||||
@if (ShowSearchButton)
|
||||
{
|
||||
<Button Color="Color.Primary" Icon="fa-solid fa-magnifying-glass" class="btn-xs ms-2" OnClickWithoutRender="SearchClick">
|
||||
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
|
||||
</Button>
|
||||
}
|
||||
<Button Color="Color.Secondary" Icon="fa-solid fa-magnifying-glass" OnClickWithoutRender="SearchClick">
|
||||
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="@TopSearchBodyClassString" id="@TopSearchBodyId">
|
||||
<div class="card-body-wrapper">
|
||||
@if(CustomerSearchModel != null && CustomerSearchTemplate != null)
|
||||
}
|
||||
<div class="table-search-buttons">
|
||||
@if (ShowResetButton)
|
||||
{
|
||||
@CustomerSearchTemplate(CustomerSearchModel)
|
||||
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" class="btn-xs ms-2" OnClickWithoutRender="ClearSearchClick">
|
||||
<span class="d-none d-sm-inline-block">@ResetSearchButtonText</span>
|
||||
</Button>
|
||||
}
|
||||
else if(SearchTemplate != null)
|
||||
@if (ShowSearchButton)
|
||||
{
|
||||
@SearchTemplate(SearchModel)
|
||||
}
|
||||
else
|
||||
{
|
||||
<CascadingValue Value="true" IsFixed="true" Name="IsSearch">
|
||||
<EditorForm Model="SearchModel" Items="GetSearchColumns()" ItemsPerRow="SearchDialogItemsPerRow" RowType="SearchDialogRowType" LabelAlign="SearchDialogLabelAlign">
|
||||
</EditorForm>
|
||||
</CascadingValue>
|
||||
<Button Color="Color.Primary" Icon="fa-solid fa-magnifying-glass" class="btn-xs ms-2" OnClickWithoutRender="SearchClick">
|
||||
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
</HeaderTemplate>
|
||||
<BodyTemplate>
|
||||
@if(CustomerSearchModel != null && CustomerSearchTemplate != null)
|
||||
{
|
||||
@CustomerSearchTemplate(CustomerSearchModel)
|
||||
}
|
||||
else if(SearchTemplate != null)
|
||||
{
|
||||
@SearchTemplate(SearchModel)
|
||||
}
|
||||
else
|
||||
{
|
||||
<CascadingValue Value="true" IsFixed="true" Name="IsSearch">
|
||||
<EditorForm Model="SearchModel" Items="GetSearchColumns()" ItemsPerRow="SearchDialogItemsPerRow" RowType="SearchDialogRowType" LabelAlign="SearchDialogLabelAlign">
|
||||
</EditorForm>
|
||||
</CascadingValue>
|
||||
}
|
||||
</BodyTemplate>
|
||||
</Card>;
|
||||
|
||||
RenderFragment RenderPageInfo =>
|
||||
@<div class="table-page-info">
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
namespace BootstrapBlazor.Components;
|
||||
|
||||
public partial class Table<TItem>
|
||||
@ -120,14 +122,6 @@ public partial class Table<TItem>
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? OnResetSearchAsync { get; set; }
|
||||
|
||||
private string? TopSearchBodyClassString => CssBuilder.Default("card-body collapse")
|
||||
.AddClass("show", !CollapsedTopSearch)
|
||||
.Build();
|
||||
|
||||
private string TopSearchBodyId => $"{Id}_search_body";
|
||||
|
||||
private string TopSearchCollapsedString => CollapsedTopSearch ? "false" : "true";
|
||||
|
||||
/// <summary>
|
||||
/// 重置查询方法
|
||||
/// </summary>
|
||||
@ -270,6 +264,18 @@ public partial class Table<TItem>
|
||||
/// <returns></returns>
|
||||
protected List<IFilterAction> GetSearchs() => Columns.Where(col => col.Searchable).ToSearchs(SearchText);
|
||||
|
||||
private async Task OnSearchKeyup(KeyboardEventArgs args)
|
||||
{
|
||||
if (args.Key == "Enter")
|
||||
{
|
||||
await SearchClick();
|
||||
}
|
||||
else if (args.Key == "Escape")
|
||||
{
|
||||
await ClearSearchClick();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置搜索按钮调用此方法
|
||||
/// </summary>
|
||||
@ -284,18 +290,4 @@ public partial class Table<TItem>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<ITableColumn> GetSearchColumns() => Columns.Where(c => c.Searchable);
|
||||
|
||||
/// <summary>
|
||||
/// 客户端 SearchTextbox 文本框内按回车时调用此方法
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[JSInvokable]
|
||||
public async Task OnSearch() => await SearchClick();
|
||||
|
||||
/// <summary>
|
||||
/// 客户端 SearchTextbox 文本框内按 ESC 时调用此方法
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[JSInvokable]
|
||||
public async Task OnClearSearch() => await ClearSearchClick();
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -35,8 +35,6 @@ export class Table extends BlazorComponent {
|
||||
if (this._isResizeColumn) {
|
||||
this._setResizeListener()
|
||||
}
|
||||
|
||||
this._setSearchListener()
|
||||
}
|
||||
|
||||
_execute(args) {
|
||||
@ -281,16 +279,6 @@ export class Table extends BlazorComponent {
|
||||
})
|
||||
}
|
||||
|
||||
_setSearchListener() {
|
||||
EventHandler.on(this._element, 'keyup', '.table-toolbar-search', e => {
|
||||
if (e.key === 'Enter') {
|
||||
this._invoker.invokeMethodAsync("OnSearch");
|
||||
} else if (e.key === 'Escape') {
|
||||
this._invoker.invokeMethodAsync("OnClearSearch");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_dispose() {
|
||||
if (this._fixedHeader) {
|
||||
EventHandler.off(this._body, 'scroll')
|
||||
@ -300,8 +288,6 @@ export class Table extends BlazorComponent {
|
||||
EventHandler.off(this._element, 'keydown')
|
||||
}
|
||||
|
||||
EventHandler.off(this._element, 'keyup', '.table-toolbar-search')
|
||||
|
||||
this._columns.forEach(col => {
|
||||
EventHandler.off(col, 'mousedown')
|
||||
EventHandler.off(col, 'touchstart')
|
||||
|
@ -370,33 +370,6 @@ public class TableTest : TableTestBase
|
||||
await cut.InvokeAsync(() => searchButton.Click());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_JSInvoke()
|
||||
{
|
||||
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
|
||||
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
|
||||
{
|
||||
pb.AddChildContent<Table<Foo>>(pb =>
|
||||
{
|
||||
pb.Add(a => a.ShowSearch, true);
|
||||
pb.Add(a => a.SearchMode, SearchMode.Top);
|
||||
pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer));
|
||||
pb.Add(a => a.TableColumns, foo => builder =>
|
||||
{
|
||||
builder.OpenComponent<TableColumn<Foo, string>>(0);
|
||||
builder.AddAttribute(1, "Field", "Name");
|
||||
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string)));
|
||||
builder.AddAttribute(3, "Searchable", true);
|
||||
builder.CloseComponent();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var table = cut.FindComponent<Table<Foo>>();
|
||||
await cut.InvokeAsync(() => table.Instance.OnSearch());
|
||||
await cut.InvokeAsync(() => table.Instance.OnClearSearch());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowToolbar_Ok()
|
||||
{
|
||||
@ -2491,41 +2464,6 @@ public class TableTest : TableTestBase
|
||||
Assert.Null(table.Instance.SearchModel.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void CollapsedTopSearch_Ok(bool collapsed)
|
||||
{
|
||||
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
|
||||
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
|
||||
{
|
||||
pb.AddChildContent<Table<Foo>>(pb =>
|
||||
{
|
||||
pb.Add(a => a.ShowSearch, true);
|
||||
pb.Add(a => a.SearchMode, SearchMode.Top);
|
||||
pb.Add(a => a.RenderMode, TableRenderMode.Table);
|
||||
pb.Add(a => a.CollapsedTopSearch, collapsed);
|
||||
pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer));
|
||||
pb.Add(a => a.TableColumns, foo => builder =>
|
||||
{
|
||||
builder.OpenComponent<TableColumn<Foo, string>>(0);
|
||||
builder.AddAttribute(1, "Field", "Name");
|
||||
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string)));
|
||||
builder.CloseComponent();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (collapsed)
|
||||
{
|
||||
cut.Contains("aria-expanded=\"false\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
cut.Contains("aria-expanded=\"true\"");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
|
Loading…
Reference in New Issue
Block a user