diff --git a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs index 32855a103d1..d3c24590968 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs @@ -61,11 +61,11 @@ public sealed partial class TreeViews private List>? AsyncItems { get; set; } - private List>? MaxItems { get; set; } = TreeFoo.GetTreeItems(); + private List> MaxItems { get; set; } = TreeFoo.GetTreeItems(); - private List>? SearchItems1 { get; set; } = TreeFoo.GetTreeItems(); + private List> SearchItems1 { get; set; } = TreeFoo.GetTreeItems(); - private List>? SearchItems2 { get; set; } = TreeFoo.GetTreeItems(); + private List> SearchItems2 { get; set; } = TreeFoo.GetTreeItems(); private List> VirtualizeItems { get; } = TreeFoo.GetVirtualizeTreeItems(); @@ -269,21 +269,57 @@ private Task SelectedItemOnChanged(SelectedItem selectedItem) return ret; } - private static async Task>?> OnSearchAsync(string searchText) + private async Task>?> OnSearchAsync(string? searchText) { await Task.Delay(20); List>? items = null; if (!string.IsNullOrEmpty(searchText)) { - items = - [ - new TreeViewItem(new TreeFoo() { Text = searchText }) { Text = searchText }, - ]; + items = GetSearchItems(searchText, SearchItems1); } return items; } + private static List> GetSearchItems(string searchText, IEnumerable> source) + { + var items = new List>(); + foreach (var item in source) + { + // 递归过滤子节点,保留层次结构 + var children = GetSearchItems(searchText, item.Items); + + // 节点自身匹配或其后代存在匹配时保留该节点 + var isMatch = !string.IsNullOrEmpty(item.Text) && item.Text.Contains(searchText, StringComparison.OrdinalIgnoreCase); + if (isMatch || children.Count > 0) + { + // 克隆节点,避免污染原始数据源 + var node = CloneItem(item); + node.Items = children; + foreach (var child in children) + { + child.Parent = node; + } + items.Add(node); + } + } + return items; + } + + private static TreeViewItem CloneItem(TreeViewItem item) => new(item.Value) + { + Text = item.Text, + Icon = item.Icon, + ExpandIcon = item.ExpandIcon, + CssClass = item.CssClass, + IsActive = item.IsActive, + IsDisabled = item.IsDisabled, + IsExpand = true, + HasChildren = item.HasChildren, + CheckedState = item.CheckedState, + Template = item.Template + }; + private static async Task>> OnExpandVirtualNodeAsync(TreeViewItem node) { await Task.Delay(500); diff --git a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs index 01cca014889..265cc73d77c 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs +++ b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs @@ -124,8 +124,8 @@ public partial class TreeView : IModelEqualityComparer public string? ClearSearchIcon { get; set; } /// - /// 获得/设置 搜索回调方法,默认为 null - /// Gets or sets the search callback method. Default is null + /// 获得/设置 搜索回调方法,默认为 null。重置搜索时以 null 作为搜索条件再次调用 + /// Gets or sets the search callback method. Default is null. It is invoked again with a null search term when the search is reset /// /// Enabled by setting to true. [Parameter] @@ -738,42 +738,25 @@ private async Task OnEnterAsync(string? searchText) private async Task OnClickSearch() { - if (OnSearchAsync != null) + _searchItems = OnSearchAsync == null ? null : await OnSearchAsync(_searchText); + _rows = null; + if (_activeItem != null) { - _rows = null; - _searchItems = await OnSearchAsync(_searchText); - if (_searchItems == null || _activeItem == null) + var item = Rows.FirstOrDefault(i => Equals(i.Value, _activeItem.Value)); + if (item != null) { - StateHasChanged(); + SetActiveItem(item); return; } - - var val = _searchItems.GetAllItems().FirstOrDefault(i => Equals(i.Value, _activeItem.Value)); - if (val == null) - { - StateHasChanged(); - return; - } - - _activeItem = val; - SetActiveItem(val.Value); } + + StateHasChanged(); } - private Task OnClickResetSearch() + private async Task OnClickResetSearch() { _searchText = null; - _searchItems = null; - _rows = null; - if (_activeItem != null && Items != null) - { - SetActiveItem(_activeItem.Value); - } - else - { - StateHasChanged(); - } - return Task.CompletedTask; + await OnClickSearch(); } /// diff --git a/test/UnitTest/Components/SelectTreeTest.cs b/test/UnitTest/Components/SelectTreeTest.cs index 8f15dc54864..eaefcc5d7b9 100644 --- a/test/UnitTest/Components/SelectTreeTest.cs +++ b/test/UnitTest/Components/SelectTreeTest.cs @@ -235,7 +235,9 @@ public async Task OnSearchAsync_Ok() builder.Add(a => a.OnSearchAsync, new Func>?>>(v => { key = v; - return Task.FromResult>?>([new TreeViewItem(new TreeFoo()) { Text = v }]); + return Task.FromResult>?>(string.IsNullOrEmpty(v) + ? null + : [new TreeViewItem(new TreeFoo()) { Text = v }]); })); builder.Add(a => a.Items, items); }); diff --git a/test/UnitTest/Components/TreeViewTest.cs b/test/UnitTest/Components/TreeViewTest.cs index 27d53e00013..52b634a04b0 100644 --- a/test/UnitTest/Components/TreeViewTest.cs +++ b/test/UnitTest/Components/TreeViewTest.cs @@ -1513,7 +1513,9 @@ public async Task Enter_Ok() pb.Add(a => a.OnSearchAsync, new Func>?>>(v => { key = v; - return Task.FromResult>?>([new TreeViewItem(new TreeFoo()) { Text = v }]); + return Task.FromResult>?>(string.IsNullOrEmpty(v) + ? null + : [new TreeViewItem(new TreeFoo()) { Text = v }]); })); pb.Add(a => a.Items, items); }); @@ -1584,25 +1586,26 @@ public async Task Search_CheckedItem_Ok() var checkbox = cut.FindComponent>>(); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Single(checkedItems!); + Assert.NotNull(checkedItems); + Assert.Single(checkedItems); Assert.Equal(items[0].Value.Id, checkedItems[0].Value.Id); Assert.Equal(CheckboxState.Checked, items[0].CheckedState); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Empty(checkedItems!); + Assert.Empty(checkedItems); Assert.Equal(CheckboxState.UnChecked, items[0].CheckedState); searchItems = [new(new TreeFoo { Id = "NotFound", Text = "NotFound" }) { Text = "NotFound" }]; await cut.InvokeAsync(() => input.Instance.OnEnterAsync!(string.Empty)); checkbox = cut.FindComponent>>(); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Empty(checkedItems!); + Assert.Empty(checkedItems); searchItems = [items[0]]; await cut.InvokeAsync(() => input.Instance.OnEnterAsync!(string.Empty)); checkbox = cut.FindComponent>>(); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Single(checkedItems!); + Assert.Single(checkedItems); Assert.Same(items[0], checkedItems[0]); } @@ -1651,11 +1654,18 @@ public async Task Search_NullBranches_Ok() { var items = TreeFoo.GetTreeItems(); List>? searchResult = [items[0]]; + var searchCount = 0; + string? searchText = string.Empty; var cut = Context.Render>(pb => { pb.Add(a => a.ShowSearch, true); pb.Add(a => a.Items, items); - pb.Add(a => a.OnSearchAsync, _ => Task.FromResult(searchResult)); + pb.Add(a => a.OnSearchAsync, text => + { + searchCount++; + searchText = text; + return Task.FromResult>?>(searchResult); + }); }); var input = cut.FindComponent>(); @@ -1669,6 +1679,8 @@ public async Task Search_NullBranches_Ok() await cut.InvokeAsync(() => input.Instance.OnEscAsync!(string.Empty)); Assert.Equal(9, cut.FindAll(".tree-content").Count); + Assert.Equal(3, searchCount); + Assert.Null(searchText); cut.Render(pb => {