From 6819c0323931a78493ed76cb3f65dfdfb4db9e40 Mon Sep 17 00:00:00 2001 From: Old Li <33386249+azlis@users.noreply.github.com> Date: Sat, 22 Jun 2024 15:39:40 +0800 Subject: [PATCH] feat(MultiFilter): add MultiFilter component (#3703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add TableContextualMenu Component * add CustomFilter * add OnSearchValueChanged * 实现单选过滤 * refactor: 增加关系运算符 * 添加隔离css * 重构代码 * 添加本地化 * 添加demo,重构代码 * 优化UI布局 * refactor: 重命名组件 * refactor: 更新 scss 源 * refactor: 移除 CustomFilter 参数 * refactor: 精简代码 * doc: 更新资源文件 * doc: 更新示例 * refactor: 更新样式 * feat: 增加 ShowSearch 参数 * refactor: 更新全选框状态 * refactor: 重置更新搜索框值 * style: 更新样式 * 固定宽高 * feat: 弃用属性不参与序列化 * doc: 更新说明文档 * test: 增加单元测试 * chore: bump version 8.6.3 --------- Co-authored-by: Argo-AscioTech --- .../Samples/Table/TablesFilter.razor | 37 ++++- src/BootstrapBlazor.Server/Locales/en-US.json | 2 + src/BootstrapBlazor.Server/Locales/zh-CN.json | 2 + src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- .../Components/Filters/BoolFilter.razor.cs | 6 +- .../Components/Filters/MultiFilter.razor | 27 +++ .../Components/Filters/MultiFilter.razor.cs | 157 ++++++++++++++++++ .../Components/Filters/MultiFilter.razor.scss | 51 ++++++ src/BootstrapBlazor/Locales/en.json | 4 + src/BootstrapBlazor/Locales/zh.json | 4 + .../Options/QueryPageOptions.cs | 4 + .../wwwroot/scss/components.scss | 1 + test/UnitTest/Components/TableFilterTest.cs | 78 +++++++++ 13 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 src/BootstrapBlazor/Components/Filters/MultiFilter.razor create mode 100644 src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs create mode 100644 src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss diff --git a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor index 649acd18b66..032f69f849d 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor @@ -46,7 +46,7 @@ + Introduction="@Localizer["FilterTemplateIntro"]" Name="CustomerFilter">
@((MarkupString)Localizer["TablesFilterTemplateDescription", ComponentSourceCodeUrl].Value)
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 3c0d02b331e..f0a825dabc6 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -5448,6 +5448,8 @@ "AutoHeightIntro": "

Highly Adaptive

In this example, when the parent container height is set to 600px expand/collapse the search bar, the table automatically fills the parent container

" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesFilter": { + "MultiFilterTitle": "Multiple selection list filtering", + "MultiFilterIntro": "Use the built-in MultiFilter component to provide multi-select filtering via FilterTemplate", "TablesFilterTitle": "Filter and sort function", "TablesFilterDesc": "Filter to quickly find the data you want to see; sort to quickly find or compare data.", "TablesFilterDescLi1": "Filters a column of data to specify the column to be filtered by specifying the filterable property of the column", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 57a7d4ac597..baa05477144 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -5448,6 +5448,8 @@ "AutoHeightIntro": "

高度自适应

本例中设置父容器高度为 600px 展开/收起搜索栏时,表格自动充满父容器

" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesFilter": { + "MultiFilterTitle": "多选列表筛选", + "MultiFilterIntro": "通过 FilterTemplate 使用内置 MultiFilter 组件提供多选筛选功能", "TablesFilterTitle": "筛选和排序功能", "TablesFilterDesc": "筛选可快速查找到自己想看的数据;排序可快速查找或对比数据。", "TablesFilterDescLi1": "对某一列数据进行筛选,通过指定列的 Filterable 属性来指定需要筛选的列", diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index a08f6dc4509..2c61cb068b1 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 8.6.3-beta06 + 8.6.3 diff --git a/src/BootstrapBlazor/Components/Filters/BoolFilter.razor.cs b/src/BootstrapBlazor/Components/Filters/BoolFilter.razor.cs index e711e4161d9..8fa59b90efb 100644 --- a/src/BootstrapBlazor/Components/Filters/BoolFilter.razor.cs +++ b/src/BootstrapBlazor/Components/Filters/BoolFilter.razor.cs @@ -39,9 +39,9 @@ protected override void OnParametersSet() Items ??= new SelectedItem[] { - new SelectedItem("", Localizer["BoolFilter.AllText"].Value), - new SelectedItem("true", Localizer["BoolFilter.TrueText"].Value), - new SelectedItem("false", Localizer["BoolFilter.FalseText"].Value) + new("", Localizer["BoolFilter.AllText"].Value), + new("true", Localizer["BoolFilter.TrueText"].Value), + new("false", Localizer["BoolFilter.FalseText"].Value) }; } diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor new file mode 100644 index 00000000000..cb78eeff800 --- /dev/null +++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor @@ -0,0 +1,27 @@ +@using Microsoft.Extensions.Localization +@namespace BootstrapBlazor.Components +@inherits FilterBase +@inject IStringLocalizer Localizer + +
+ @if (ShowSearch) + { + + } +
+
+ +
+
+ @foreach (var item in GetItems()) + { +
+ +
+ } +
+
+
diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs new file mode 100644 index 00000000000..5e8dcf5336e --- /dev/null +++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs @@ -0,0 +1,157 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// 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/ + +namespace BootstrapBlazor.Components; + +/// +/// 表格过滤菜单组件 +/// +public partial class MultiFilter +{ + /// + /// 获得/设置 搜索栏占位符 默认 nul 使用资源文件中值 + /// + [Parameter] + public string? SearchPlaceHolderText { get; set; } + + /// + /// 获得/设置 全选按钮文本 默认 nul 使用资源文件中值 + /// + [Parameter] + public string? SelectAllText { get; set; } + + /// + /// 获得/设置 是否显示搜索栏 默认 true + /// + [Parameter] + public bool ShowSearch { get; set; } = true; + + private string? _searchText; + + private bool checkAll = false; + + private readonly List _source = []; + + private IEnumerable? _items; + + /// + /// OnInitialized 方法 + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + if (Items != null) + { + _source.AddRange(Items.Select(item => new MultiFilterItem() { Value = item.Value, Text = item.Text })); + } + if (TableFilter != null) + { + TableFilter.ShowMoreButton = false; + } + } + + /// + /// + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + SearchPlaceHolderText ??= Localizer["MultiFilterSearchPlaceHolderText"]; + SelectAllText ??= Localizer["MultiFilterSelectAllText"]; + } + + /// + /// 重置过滤条件方法 + /// + public override void Reset() + { + checkAll = false; + _searchText = string.Empty; + foreach (var item in _source) + { + item.Checked = false; + } + StateHasChanged(); + } + + /// + /// 生成过滤条件方法 + /// + /// + public override FilterKeyValueAction GetFilterConditions() + { + var filter = new FilterKeyValueAction() { Filters = [], FilterLogic = FilterLogic.Or }; + + foreach (var item in GetItems().Where(i => i.Checked)) + { + filter.Filters.Add(new FilterKeyValueAction() + { + FieldKey = FieldKey, + FieldValue = item.Value, + FilterAction = FilterAction.Equal + }); + } + return filter; + } + + private CheckboxState GetState() => GetItems().All(i => i.Checked) + ? CheckboxState.Checked + : GetItems().All(i => !i.Checked) ? CheckboxState.UnChecked : CheckboxState.Indeterminate; + + private Task OnStateChanged(CheckboxState state, bool val) + { + checkAll = val; + if (state == CheckboxState.Checked) + { + foreach (var item in _source) + { + item.Checked = true; + } + } + else + { + foreach (var item in _source) + { + item.Checked = false; + } + } + StateHasChanged(); + return Task.CompletedTask; + } + + /// + /// 过滤内容搜索 + /// + /// + /// + private Task OnSearchValueChanged(string? val) + { + _searchText = val; + if (!string.IsNullOrEmpty(_searchText)) + { + _items = _source.Where(i => i.Text.Contains(_searchText)); + } + else + { + _items = null; + } + StateHasChanged(); + return Task.CompletedTask; + } + + private IEnumerable GetItems() => _items ?? _source; + + class MultiFilterItem + { + public bool Checked { get; set; } + + [NotNull] + public string? Value { get; init; } + + [NotNull] + public string? Text { get; init; } + } +} diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss new file mode 100644 index 00000000000..f45f056c898 --- /dev/null +++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss @@ -0,0 +1,51 @@ +.bb-multi-filter { + --bb-multi-filter-height: 180px; + --bb-multi-filter-width: 224px; + --bb-multi-filter-search-margin-bottom: 1rem; + --bb-multi-filter-body-item-bg: #fff; + --bb-multi-filter-body-item-hover-bg: #fff; + --bb-multi-filter-body-item-margin: .5rem; + + .bb-multi-filter-search { + margin-bottom: var(--bb-multi-filter-search-margin-bottom); + } + + .bb-multi-filter-list { + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + padding: var(--bb-multi-filter-body-item-margin); + + .bb-multi-filter-header { + margin-bottom: var(--bb-multi-filter-body-item-margin); + padding-bottom: var(--bb-multi-filter-body-item-margin); + border-bottom: var(--bs-border-width) solid var(--bs-border-color); + } + + .bb-multi-filter-body { + height: var(--bb-multi-filter-height); + width: var(--bb-multi-filter-width); + margin-top: var(--bb-multi-filter-body-item-margin); + + .bb-multi-filter-body-item { + background-color: var(--bb-multi-filter-body-item-bg); + + .form-check { + width: 100%; + + .form-check-input + .form-check-label { + text-overflow: unset; + overflow: unset; + } + } + + &:not(:last-child) { + margin-bottom: var(--bb-multi-filter-body-item-margin); + } + + &:hover { + background-color: var(--bb-multi-filter-body-item-hover-bg); + } + } + } + } +} diff --git a/src/BootstrapBlazor/Locales/en.json b/src/BootstrapBlazor/Locales/en.json index 21d20f2dd2e..693b928fb20 100644 --- a/src/BootstrapBlazor/Locales/en.json +++ b/src/BootstrapBlazor/Locales/en.json @@ -173,6 +173,10 @@ "CloseAllTabsText": "Close All", "NotFoundTabText": "NotFound" }, + "BootstrapBlazor.Components.MultiFilter": { + "MultiFilterSearchPlaceHolderText": "Please enter ...", + "MultiFilterSelectAllText": "Select All" + }, "BootstrapBlazor.Components.Table": { "AddButtonText": "Add", "EditButtonText": "Edit", diff --git a/src/BootstrapBlazor/Locales/zh.json b/src/BootstrapBlazor/Locales/zh.json index 9cb887b99b7..ecb2e3fb7b9 100644 --- a/src/BootstrapBlazor/Locales/zh.json +++ b/src/BootstrapBlazor/Locales/zh.json @@ -173,6 +173,10 @@ "CloseAllTabsText": "关闭所有标签", "NotFoundTabText": "未找到" }, + "BootstrapBlazor.Components.MultiFilter": { + "MultiFilterSearchPlaceHolderText": "请输入 ...", + "MultiFilterSelectAllText": "全选" + }, "BootstrapBlazor.Components.Table": { "AddButtonText": "新建", "EditButtonText": "编辑", diff --git a/src/BootstrapBlazor/Options/QueryPageOptions.cs b/src/BootstrapBlazor/Options/QueryPageOptions.cs index 5e2bcddc218..b1c38b996c4 100644 --- a/src/BootstrapBlazor/Options/QueryPageOptions.cs +++ b/src/BootstrapBlazor/Options/QueryPageOptions.cs @@ -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 System.Text.Json.Serialization; + namespace BootstrapBlazor.Components; /// @@ -82,6 +84,7 @@ public class QueryPageOptions /// [Obsolete("This property is obsolete. Use CustomerSearches instead. 已过期,请使用 CustomerSearches 参数")] [ExcludeFromCodeCoverage] + [JsonIgnore] public List CustomerSearchs => CustomerSearches; /// @@ -112,6 +115,7 @@ public class QueryPageOptions /// 组件首次查询数据时为 true [Obsolete("This property is obsolete. Use IsFirstQuery. 已弃用单词拼写错误,请使用 IsFirstQuery")] [ExcludeFromCodeCoverage] + [JsonIgnore] public bool IsFristQuery { get => IsFirstQuery; set => IsFirstQuery = value; } /// diff --git a/src/BootstrapBlazor/wwwroot/scss/components.scss b/src/BootstrapBlazor/wwwroot/scss/components.scss index 2982b20d405..d4f7ab28cbb 100644 --- a/src/BootstrapBlazor/wwwroot/scss/components.scss +++ b/src/BootstrapBlazor/wwwroot/scss/components.scss @@ -39,6 +39,7 @@ @import "../../Components/ErrorLogger/ErrorLogger.razor.scss"; @import "../../Components/FlipClock/FlipClock.razor.scss"; @import "../../Components/FileIcon/FileIcon.razor.scss"; +@import "../../Components/Filters/MultiFilter.razor.scss"; @import "../../Components/Filters/TableFilter.razor.scss"; @import "../../Components/Footer/Footer.razor.scss"; @import "../../Components/FullScreen/FullScreenButton.razor.scss"; diff --git a/test/UnitTest/Components/TableFilterTest.cs b/test/UnitTest/Components/TableFilterTest.cs index a5ed1402465..f3ce8a4caef 100644 --- a/test/UnitTest/Components/TableFilterTest.cs +++ b/test/UnitTest/Components/TableFilterTest.cs @@ -78,6 +78,82 @@ public void FilterTemplate_Ok() }); } + [Fact] + public async Task MultiFilter_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Items, new List + { + new() + }); + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.TableColumns, cat => builder => + { + var index = 0; + builder.OpenComponent>(index++); + builder.AddAttribute(index++, nameof(TableColumn.Field), cat.P8); + builder.AddAttribute(index++, nameof(TableColumn.FieldExpression), Utility.GenerateValueExpression(cat, nameof(Cat.P8), typeof(string))); + builder.AddAttribute(index++, nameof(TableColumn.Filterable), true); + builder.AddAttribute(index++, nameof(TableColumn.FilterTemplate), new RenderFragment(b => + { + b.OpenComponent(0); + b.AddAttribute(1, nameof(MultiFilter.ShowSearch), true); + b.AddAttribute(2, nameof(MultiFilter.Items), new SelectedItem[] { + new("test1", "test1"), + new("test2", "test2") + }); + b.CloseComponent(); + })); + builder.CloseComponent(); + }); + }); + }); + + cut.DoesNotContain("multi-filter-placeholder"); + cut.DoesNotContain("multi-filter-All"); + + var filter = cut.FindComponent(); + filter.SetParametersAndRender(pb => + { + pb.Add(a => a.SearchPlaceHolderText, "multi-filter-placeholder"); + pb.Add(a => a.SelectAllText, "multi-filter-All"); + }); + cut.Contains("multi-filter-placeholder"); + cut.Contains("multi-filter-All"); + + await cut.InvokeAsync(() => filter.Instance.Reset()); + + // 选中选项 + var checkboxs = cut.FindComponents>(); + Assert.Equal(3, checkboxs.Count); + await cut.InvokeAsync(() => checkboxs[2].Instance.SetState(CheckboxState.Checked)); + + FilterKeyValueAction? action = null; + await cut.InvokeAsync(() => + { + action = filter.Instance.GetFilterConditions(); + }); + Assert.NotNull(action); + Assert.Equal(FilterLogic.Or, action.FilterLogic); + Assert.NotNull(action.Filters); + Assert.NotNull(action.Filters[0]); + Assert.Equal("test2", action.Filters[0].FieldValue); + Assert.Equal("P8", action.Filters[0].FieldKey); + Assert.Equal(CheckboxState.Indeterminate, checkboxs[0].Instance.State); + + // 测试全选 + await cut.InvokeAsync(() => checkboxs[0].Instance.SetState(CheckboxState.Checked)); + await cut.InvokeAsync(() => checkboxs[0].Instance.SetState(CheckboxState.UnChecked)); + + // 测试搜索 + var input = cut.Find(".bb-multi-filter-search"); + await cut.InvokeAsync(() => input.Input("test02")); + await cut.InvokeAsync(() => input.Input("")); + } + [Fact] public void NotInTable_Ok() { @@ -240,5 +316,7 @@ private class Cat public decimal P6 { get; set; } public Foo? P7 { get; set; } + + public string? P8 { get; set; } } }