<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画页码,别在服务端一次拉全量回前端。
相关
- 服务契约与代理见前后端契约。
- 完整 CRUD(新增/编辑/删除)见写第一个 CRUD 页面。
Result/PagingResult语义与数据层见数据层与 Database 契约。
扫码关注公众号
扫码加入 QQ 群