表格分页与筛选

Table + PagingCriteria + PagingResult:服务端分页、列排序、条件筛选与批量操作的标准写法。

最后更新

<Table> 是 Known 里最"重"的组件之一。核心决策只有一条:分页发生在服务端,客户端组件只渲染"当前页这一批数据",绝不把整张表一次性塞进 Items。

查询契约:条件进 PagingCriteria,结果出 PagingResult<T>

框架没有 TableQuery / PagedResult 这类自定义基类——分页、排序、过滤全塞进现成的 PagingCriteria,返回统一是 PagingResult<T>:

// 已知类型,无需自己定义
// PagingCriteria:PageIndex(默认1) / PageSize(默认20) / OrderBys(string[]) / Parameters(字典)
// PagingResult<T> :Total(int) / Data(List<T>) / IsValid / Message

约定:

  • 翻页:PageIndex 从 1 起(不是 0),PageSize 每页条数。
  • 排序:把列名交给 OrderBys,如 ["Amount desc"];框架解析 desc/asc 关键字并把属性名映射到列名(非法标识符会被丢弃,防注入)。
  • 过滤:Parameters 里非空项会被拼成 等值 条件 列名=@键名(键名前的 @ 会被剥掉,再映射到实体字段)。所以 Parameters["@Status"] = "Paid" → where Status=@Status。要做"包含"这类模糊匹配,Parameters 帮不了你——改走带自定义 SQL 的重载 db.QueryPageAsync<Order>(sql, criteria),在你的 sql 里自己写 like。

契约 = 接口 + 实现(同一个文件)

[WebApi]
public interface IOrderService
{
    Task<PagingResult<Order>> QueryOrdersAsync(PagingCriteria criteria);
    Task<Result> DeleteOrdersAsync(List<string> ids);
}

[Service]
class OrderService(Database db) : IOrderService
{
    // 标准查询写法:服务层只一行委托,分页/排序/过滤都交给框架按 criteria 拼 SQL
    public Task<PagingResult<Order>> QueryOrdersAsync(PagingCriteria criteria)
        => db.QueryPageAsync<Order>(criteria);

    public async Task<Result> DeleteOrdersAsync(List<string> ids)
    {
        if (ids == null || ids.Count == 0)
            return Result.Error("请选择要删除的订单!");
        return await db.TransactionAsync("删除", async tx =>
        {
            foreach (var id in ids)
                await tx.DeleteAsync<Order>(id);
        });
    }
}

服务里不要手写 Where / OrderBy / Skip / Take——那是 EF 的思路。Known 是 ADO.NET 微 ORM,把 criteria 原样丢给 QueryPageAsync,排序分页由数据库方言自己完成。

客户端

条件由页面负责组装,<Table> 负责把翻页、列排序、勾选回传给页面:

@page "/orders"
@inherits BasePage
@attribute [Menu("订单", Icon = "cart")]
@inject IOrderService Orders
@inject UIService UI

<Table TItem="Order"
       Columns="_columns"
       Items="_items"
       Loading="_loading"
       Striped="true"
       Selectable="true"
       @bind-SelectedItems="_selected"
       ShowPagination="true"
       Total="_total"
       Page="_page"
       PageSize="_pageSize"
       PageChanged="OnPageChanged"
       OnSortChanged="OnSortChanged">
    <Toolbar>
        <Input Value="_status" ValueChanged="v => _status = v" Live="true" Placeholder="按状态精确筛选" />
        <Button Text="查询" Kind="primary" OnClick="() => Load(1)" />
    </Toolbar>
    <BatchActions>
        <Button Text="批量删除" Kind="danger" OnClick="DeleteSelected" />
    </BatchActions>
    <RowActions Context="row">
        <Button Text="编辑" Kind="text" OnClick="() => Edit(row)" />
    </RowActions>
</Table>

@code {
    List<Order> _items = [];
    List<Order> _selected = [];
    int _page = 1, _pageSize = 20, _total;
    bool _loading;
    string _status;
    string[] _orderBys;

    List<TableColumn> _columns =
    [
        new() { Field = "Customer", Title = "客户", Sortable = true },
        new() { Field = "Amount",   Title = "金额", Sortable = true, Align = "right" },
        new() { Field = "Status",   Title = "状态", Filterable = true },
    ];

    protected override async Task OnAfterRenderAsync(bool first)
    {
        if (first) await Load(1);
    }

    private async Task Load(int page)
    {
        _page = page;
        _loading = true;
        try
        {
            var criteria = new PagingCriteria { PageIndex = page, PageSize = _pageSize };
            if (!string.IsNullOrWhiteSpace(_status))
                criteria.Parameters["@Status"] = _status;      // 等值过滤
            if (_orderBys is { Length: > 0 })
                criteria.OrderBys = _orderBys;                  // 列排序

            var result = await Orders.QueryOrdersAsync(criteria);
            _items = result.Data ?? [];
            _total = result.Total;
            if (!result.IsValid) UI.Error(result.Message);
        }
        finally { _loading = false; StateHasChanged(); }
    }

    private Task OnPageChanged(int page) => Load(page);

    private Task OnSortChanged(TableSort sort)
    {
        // 列头排序 → 分页条件的 OrderBys;取消排序就清空回默认排序
        _orderBys = sort.Active && !string.IsNullOrEmpty(sort.Field)
            ? [$"{sort.Field} {(sort.Desc ? "desc" : "asc")}"]
            : null;
        return Load(1);   // 换排序条件后回到第一页
    }

    private async Task DeleteSelected()
    {
        if (_selected.Count == 0) { UI.Error("请先勾选要删除的订单"); return; }
        var r = await Orders.DeleteOrdersAsync(_selected.Select(o => o.Id).ToList());
        if (r.IsValid) { UI.Success(r.Message); await Load(_page); }
        else UI.Error(r.Message);
    }

    private void Edit(Order row) { /* 弹 Modal 编辑,回调里 Orders.SaveOrderAsync(...) 后 Load */ }
}

<Table> 的关键参数就这几个:Columns(列定义列表,或把 <Column> 直接写进 <Table> 的标记内容里,列与单元格模板都留在标记)+ Items(当前页数据)驱动渲染,ShowPagination / Total / Page / PageSize / PageChanged 做服务端分页,OnSortChanged(回调参数 TableSort{ Field, Desc, Active })做列排序,Selectable + @bind-SelectedItems 拿勾选项,Toolbar / BatchActions / RowActions 是三个片段槽。客户端小数据量想省事,可开 AutoPaging="true" 让组件自己按 Items 全量切片、免去外部翻页。

TableColumn 的属性是 Field(写实体属性名,组件靠反射取值)、Title、Width、Align、Sortable、Filterable、Ellipsis、Fixed、MergeRows,自定义单元格用 Cell(入参 TableCellContext,可 ctx.Get<T>() 取值)。

行内编辑与批量操作

<Table> 本身没有"双击单元格就地编辑"的内建开关;行内编辑要么是你在 Cell 片段里放一个绑定控件、自己写回,要么(推荐)点行内「编辑」按钮弹 <Modal> 表单——和写第一个 CRUD 页面里那套一致。批量操作走 Selectable="true" 打开行首复选框,@bind-SelectedItems 拿到选中列表,<BatchActions> 片段放批量按钮;删完记得回 Load 重取当前页。

常见坑

  • 改筛选 / 排序后忘了回第一页:条件变了仍停在第 5 页,很可能落进空页。Load(1) 才是对的。
  • Parameters 当模糊查询用:它只会拼成 列=@值 的等值匹配,塞 %关键字% 匹配不到;模糊要改走自定义 sql 的 QueryPageAsync<T>(sql, criteria)。
  • PageIndex 从 0 起:与框架约定不符,从 1 起。
  • 排序字段名对不上:OrderBys / TableSort.Field 用的是实体属性名(如 Amount),要和 TableColumn.Field 一致;对不上会被框架当成非法标识符丢弃,表现就是"点了列头没反应"。
  • 指望 Items 装整张表:Items 只是当前页;总数靠 Total 画页码,别在服务端一次拉全量回前端。

相关

微信公众号二维码 扫码关注公众号
QQ 交流群二维码 扫码加入 QQ 群