Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ac41bc6
refactor: 增加 SearchForm 表单组件
ArgoZhang Feb 28, 2026
f9927dc
feat: 增加 MetaData 实现类
ArgoZhang Mar 7, 2026
53ed830
feat: 增加 CreateRenderFragment 扩展方法
ArgoZhang Mar 7, 2026
abae7e5
doc: 增加 header 文本
ArgoZhang Mar 7, 2026
fe08d75
feat: 允许 SearchModel 可为空
ArgoZhang Mar 7, 2026
805cbf6
refactor: 更新注释
ArgoZhang Mar 8, 2026
57296fa
feat: 增加 UseSearchForm 逻辑
ArgoZhang Mar 8, 2026
8e4b632
feat: 增加 ParseSearchItem 扩展方法
ArgoZhang Mar 8, 2026
7f4dbda
refactor: 精简代码
ArgoZhang Mar 8, 2026
f082bc9
fix(Table): 修复 IsTriggerByPagination 未赋值问题
ArgoZhang Mar 8, 2026
d53e834
Merge branch 'main' into feat-search
ArgoZhang Mar 8, 2026
2f4948c
Merge branch 'main' into feat-search
ArgoZhang Mar 13, 2026
c39367f
feat: 实现 ToSearches 扩展方法
ArgoZhang Mar 14, 2026
b7a853b
refactor: 更改数据类型
ArgoZhang Mar 14, 2026
8b31414
doc: 增加注释
ArgoZhang Mar 14, 2026
8a24b4d
refactor: 更新条件
ArgoZhang Mar 14, 2026
684a19d
doc: 更新示例
ArgoZhang Mar 14, 2026
f6ec337
refactor: 更改字符串条件更改为包含
ArgoZhang Mar 14, 2026
06bc8bf
doc: 更新资源文件
ArgoZhang Mar 14, 2026
1378e2e
feat: 增加 SearchMetaData 接口
ArgoZhang Mar 14, 2026
fd54355
refactor: 重命名
ArgoZhang Mar 14, 2026
e8d068d
doc: 更新示例
ArgoZhang Mar 14, 2026
5cbbafd
refactor: 更改参数名称
ArgoZhang Mar 14, 2026
bc35dab
doc: 更新文档
ArgoZhang Mar 14, 2026
fa0b0ee
refactor: 调整序列
ArgoZhang Mar 14, 2026
acfc941
feat: 增加 SearchFormItemMetaData 支持逻辑
ArgoZhang Mar 14, 2026
741d758
doc: 更新示例
ArgoZhang Mar 14, 2026
2e00f70
doc: 增加注释
ArgoZhang Mar 14, 2026
4330d6c
refactor: 重构代码
ArgoZhang Mar 14, 2026
4e53a18
doc: 更新注释文档
ArgoZhang Mar 14, 2026
55cff0f
refactor: 使用转型后的数据类型
ArgoZhang Mar 14, 2026
bac95ac
refactor: 优化代码
ArgoZhang Mar 14, 2026
b0c81fe
test: 增加 Searches 单元测试
ArgoZhang Mar 14, 2026
62c50f1
refactor: 移除 ValueType 参数
ArgoZhang Mar 14, 2026
b98dfea
test: 增加单元测试
ArgoZhang Mar 14, 2026
71bbd5c
test: 增加单元测试
ArgoZhang Mar 14, 2026
a3a258c
refactor: 重构代码
ArgoZhang Mar 14, 2026
404540c
Merge branch 'main' into feat-search
ArgoZhang Mar 14, 2026
24774c4
test: 更新单元测试
ArgoZhang Mar 14, 2026
c0216a7
feat: 增加 bb-search-form 样式
ArgoZhang Mar 14, 2026
5a1318f
test: 更新单元测试
ArgoZhang Mar 14, 2026
11e8c2e
refactor: 增加 WIP 注释
ArgoZhang Mar 14, 2026
1663bcd
test: 更新单元测试
ArgoZhang Mar 14, 2026
090bdba
refactor: 增加必填标签
ArgoZhang Mar 15, 2026
f1c8ef4
doc: 更新示例
ArgoZhang Mar 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/BootstrapBlazor/Components/SearchForm/ISearchItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

namespace BootstrapBlazor.Components;

/// <summary>
/// <para lang="zh">SearchItem 接口</para>
/// <para lang="en">SearchItem interface</para>
/// </summary>
public interface ISearchItem
{
/// <summary>
/// <para lang="zh">获得 绑定列的类型</para>
/// <para lang="en">Gets the type of the bound column</para>
/// </summary>
Type PropertyType { get; }

/// <summary>
/// <para lang="zh">获得/设置 表头显示文本</para>
/// <para lang="en">Gets or sets the header display text</para>
/// </summary>
string? Text { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 字段名称</para>
/// <para lang="en">Gets or sets the field name</para>
/// </summary>
string FieldName { get; }

/// <summary>
/// <para lang="zh">获得/设置 是否显示前置标签 默认为 null 未设置时默认显示标签</para>
/// <para lang="en">Gets or sets Whether to Show Label. Default is null, show label if not set</para>
/// </summary>
public bool? ShowLabel { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 是否显示标签提示,常用于标签文本过长被截断时,默认为 null</para>
/// <para lang="en">Gets or sets whether to show the label tooltip, usually when the label text is too long and truncated. Default is null</para>
/// </summary>
bool? ShowLabelTooltip { get; set; }
/// <summary>
/// <para lang="zh">获得/设置 当前属性的分组名称</para>
/// <para lang="en">Gets or sets the group name of the current property</para>
/// </summary>
string? GroupName { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 当前属性的分组顺序,默认为 0</para>
/// <para lang="en">Gets or sets the group order of the current property. Default is 0</para>
/// </summary>
int GroupOrder { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 顺序号</para>
/// <para lang="en">Gets or sets the order number</para>
/// </summary>
int Order { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 字段的列跨度,默认为 0</para>
/// <para lang="en">Gets or sets the field column span. Default is 0</para>
/// </summary>
int Cols { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 搜索元数据</para>
/// <para lang="en">Gets or sets the search metadata</para>
/// </summary>
public ISearchMetaData? MetaData { get; set; }

/// <summary>
/// <para lang="zh">获得 过滤器实例</para>
/// <para lang="en">Gets the filter instance</para>
/// </summary>
/// <returns>
/// <para lang="zh">过滤器实例</para>
/// <para lang="en">Filter instance</para>
/// </returns>
public FilterKeyValueAction? GetFilter() => MetaData?.GetFilter(FieldName);
}
63 changes: 63 additions & 0 deletions src/BootstrapBlazor/Components/SearchForm/SearchForm.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapComponentBase

<div @attributes="AdditionalAttributes" class="@ClassString">
<CascadingValue Value="this" IsFixed="true">
<div class="form-body">
@if (ShowUnsetGroupItemsOnTop)
{
if (UnsetGroupItems.Any())
{
@RenderUnsetGroupItems
}
@foreach (var g in GroupItems)
{
@RenderGroupItems(g)
}
}
else
{
@foreach (var g in GroupItems)
{
@RenderGroupItems(g)
}
if (UnsetGroupItems.Any())
{
@RenderUnsetGroupItems
}
}
</div>

@if (Buttons != null)
{
<div class="bb-editor-footer form-footer">
@Buttons
</div>
}
</CascadingValue>
</div>

@code
{
RenderFragment RenderUnsetGroupItems =>
@<div class="@FormClassString" style="@FormStyleString">
@foreach (var item in UnsetGroupItems)
{
<div class="@GetCssString(item)">
@AutoGenerateTemplate(item)
</div>
}
</div>;

RenderFragment<KeyValuePair<string, IOrderedEnumerable<ISearchItem>>> RenderGroupItems => g =>
@<GroupBox Title="@g.Key">
<div class="@FormClassString" style="@FormStyleString">
@foreach (var item in g.Value)
{
<div class="@GetCssString(item)">
@AutoGenerateTemplate(item)
</div>
}
</div>
</GroupBox>;
}
195 changes: 195 additions & 0 deletions src/BootstrapBlazor/Components/SearchForm/SearchForm.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

namespace BootstrapBlazor.Components;

/// <summary>
/// <para lang="zh">搜索表单类</para>
/// <para lang="en">Search Form Component</para>
/// </summary>
public partial class SearchForm : IShowLabel, IDisposable
{
/// <summary>
/// <para lang="zh">获得/设置 过滤器实例</para>
/// <para lang="en">Gets or sets the filter instance</para>
/// </summary>
[Parameter]
[NotNull]
public FilterKeyValueAction? Filter { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 过滤器改变回调事件</para>
/// <para lang="en">Gets or sets the filter changed callback event</para>
/// </summary>
[Parameter]
public EventCallback<FilterKeyValueAction> FilterChanged { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 过滤器改变回调事件 Func 版本</para>
/// <para lang="en">Gets or sets the filter changed callback event Func version</para>
/// </summary>
[Parameter]
public Func<FilterKeyValueAction, Task>? OnFilterChanged { get; set; }

Comment on lines +18 to +36
/// <summary>
/// <para lang="zh">获得/设置 每行显示组件数量 默认为 null</para>
/// <para lang="en">Gets or sets Items Per Row. Default is null</para>
/// </summary>
[Parameter]
public int? ItemsPerRow { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 设置行格式 默认 Row 布局</para>
/// <para lang="en">Gets or sets Row Type. Default is Row</para>
/// </summary>
[Parameter]
public RowType RowType { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 设置 <see cref="RowType" /> Inline 模式下标签对齐方式 默认 None 等效于 Left 左对齐</para>
/// <para lang="en">Gets or sets Label Alignment in <see cref="RowType" /> Inline mode. Default is None, equivalent to Left</para>
/// </summary>
[Parameter]
public Alignment LabelAlign { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 标签宽度 默认 null 未设置使用全局设置 <code>--bb-row-label-width</code> 值</para>
/// <para lang="en">Gets or sets Label Width. Default is null, use global setting <code>--bb-row-label-width</code> if not set</para>
/// </summary>
[Parameter]
public int? LabelWidth { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 按钮模板</para>
/// <para lang="en">Gets or sets Buttons Template</para>
/// </summary>
[Parameter]
public RenderFragment? Buttons { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 是否显示前置标签 默认为 null 未设置时默认显示标签</para>
/// <para lang="en">Gets or sets Whether to Show Label. Default is null, show label if not set</para>
/// </summary>
[Parameter]
public bool? ShowLabel { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 是否显示标签 Tooltip 多用于标签文字过长导致裁减时使用 默认 null</para>
/// <para lang="en">Gets or sets Whether to Show Label Tooltip. Default is null</para>
/// </summary>
[Parameter]
public bool? ShowLabelTooltip { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 绑定字段信息集合</para>
/// <para lang="en">Gets or sets the items collection.</para>
/// </summary>
[Parameter]
[EditorRequired]
[NotNull]
public IEnumerable<ISearchItem>? Items { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 未设置 GroupName 编辑项是否放置在顶部 默认 false</para>
/// <para lang="en">Gets or sets Whether to show unset GroupName items on top. Default is false</para>
/// </summary>
[Parameter]
public bool ShowUnsetGroupItemsOnTop { get; set; }

/// <summary>
/// <para lang="zh">获得/设置 分组类型 默认 <see cref="EditorFormGroupType.GroupBox"/></para>
/// <para lang="en">Gets or sets group type. Default is <see cref="EditorFormGroupType.GroupBox"/></para>
/// </summary>
[Parameter]
public EditorFormGroupType GroupType { get; set; }

private string? ClassString => CssBuilder.Default("bb-editor")
.AddClass("bb-editor-group-row-header", GroupType == EditorFormGroupType.RowHeader)
.AddClassFromAttributes(AdditionalAttributes)
.Build();

private string? FormClassString => CssBuilder.Default("row g-3")
.AddClass("form-inline", RowType == RowType.Inline)
.AddClass("form-inline-end", RowType == RowType.Inline && LabelAlign == Alignment.Right)
.AddClass("form-inline-center", RowType == RowType.Inline && LabelAlign == Alignment.Center)
.Build();

private string? FormStyleString => CssBuilder.Default()
.AddClass($"--bb-row-label-width: {LabelWidth}px;", LabelWidth.HasValue)
.Build();

private IEnumerable<ISearchItem> UnsetGroupItems => Items.Where(i => string.IsNullOrEmpty(i.GroupName));

private IEnumerable<KeyValuePair<string, IOrderedEnumerable<ISearchItem>>> GroupItems => Items
.Where(i => !string.IsNullOrEmpty(i.GroupName))
.GroupBy(i => i.GroupName).OrderBy(i => i.Key)
.Select(i => new KeyValuePair<string, IOrderedEnumerable<ISearchItem>>(i.First().GroupName!, i.OrderBy(x => x.Order)));

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnParametersSet()
{
base.OnParametersSet();

Items ??= [];
}

private RenderFragment AutoGenerateTemplate(ISearchItem item)
{
item.ShowLabel ??= ShowLabel;
item.ShowLabelTooltip ??= ShowLabelTooltip;
item.MetaData?.ValueChanged ??= BuildFilter;
return item.CreateRenderFragment();
}

private async Task BuildFilter()
{
Filter = new FilterKeyValueAction()
{
Filters = []
};

foreach (var item in Items)
{
var filter = item.GetFilter();
if (filter != null)
{
Filter.Filters.Add(filter);
}
}

if (FilterChanged.HasDelegate)
{
await FilterChanged.InvokeAsync(Filter);
}
if (OnFilterChanged != null)
{
await OnFilterChanged.Invoke(Filter);
}
}

private string? GetCssString(ISearchItem item)
{
int cols = Math.Max(0, Math.Min(12, item.Cols));
double mdCols = 6;
if (ItemsPerRow.HasValue)
{
mdCols = Math.Max(0, Math.Min(12, Math.Ceiling(12d / ItemsPerRow.Value)));
Comment on lines +185 to +191
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Potential division-by-zero in GetCssString when ItemsPerRow is 0

mdCols uses Math.Ceiling(12d / ItemsPerRow.Value) without handling ItemsPerRow == 0. Since ItemsPerRow is nullable and externally set, a value of 0 will cause a DivideByZeroException. Please either validate that ItemsPerRow is at least 1 (e.g., clamp to 1) or treat 0 as equivalent to null before the division.

}
return CssBuilder.Default("col-12")
.AddClass($"col-sm-{cols}", cols > 0)
.AddClass($"col-sm-6 col-md-{mdCols}", mdCols > 0 && cols == 0 && !Utility.IsCheckboxList(item.PropertyType))
.Build();
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
51 changes: 51 additions & 0 deletions src/BootstrapBlazor/Components/SearchForm/SearchForm.razor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.bb-editor {
position: relative;

.ef-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bs-body-bg);
}

&.bb-editor-group-row-header {
.groupbox {
--bb-groupbox-padding: 0;
--bb-groupbox-legend-top: 0;
--bb-groupbox-legend-left: 0;
--bb-groupbox-divider-color: var(--bs-border-color);
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
border: 0;

.legend {
writing-mode: vertical-rl;
text-orientation: mixed;
position: relative;
color: var(--bb-groupbox-divider-color);
}

.row {
margin-inline-start: 1rem;
flex: 1;
width: 1%;
min-width: 0;
position: relative;

&::before {
content: "";
width: 1px;
background-color: var(--bb-groupbox-divider-color);
position: absolute;
top: 1rem;
bottom: 0;
left: -0.5rem;
}
}
}
}
}
Comment on lines +1 to +51
Loading
Loading