aardio 文档

Table 数据管理页

本页给出一个完整的 Table + Pagination 数据管理页范例。重点不是堆叠控件,而是展示一条稳定的数据管线:

  1. 业务数组保存真实数据。
  2. 搜索、筛选、排序都作用在业务数组上。
  3. 当前页切片后构造 System.Data.DataTable
  4. Table.DataSource 只绑定当前页 DataTable。
  5. Table 事件再把修改回写到业务数组。

范例使用 aardio 设计器推荐模式:每个 AntdUI 控件放在独立 custom 宿主中,窗口布局、DPI 缩放、边距固定交给 /*DSG{{*/ ... /*}}*/ 维护。不要把大量 .NET SetBounds 绝对坐标当作普通业务窗口模板。

完整示例:设备运维数据台

import win.ui;
/*DSG{{*/
var winform = win.form(text="设备运维数据台";right=980;bottom=640;bgcolor=0xF7F8FA)
winform.add(
pageHost={cls="custom";left=20;top=592;right=960;bottom=626;db=1;dl=1;dr=1;z=6};
queryHost={cls="custom";left=20;top=18;right=260;bottom=54;dl=1;dt=1;edge=1;z=1};
resetHost={cls="custom";text="重置";left=430;top=18;right=520;bottom=54;dl=1;dt=1;frame=1;z=3};
statusHost={cls="custom";left=270;top=18;right=420;bottom=54;dl=1;dt=1;edge=1;z=2};
summaryHost={cls="custom";left=530;top=18;right=960;bottom=54;dl=1;dr=1;dt=1;edge=1;z=4};
tableHost={cls="custom";left=20;top=68;right=960;bottom=580;db=1;dl=1;dr=1;dt=1;edge=1;z=5}
)
/*}}*/

import dotNet.AntdUI;
import System;
import System.Data;

var query = AntdUI.Input(winform.queryHost);
query.PlaceholderText = "搜索设备名称或编号";
query.AllowClear = true;
query.PrefixText = "搜索";

var statusPicker = AntdUI.Select(winform.statusHost);
statusPicker.PlaceholderText = "状态";
statusPicker.AllowClear = false;
var statusOptions = [
    {text="全部状态";value=""},
    {text="运行";value="运行"},
    {text="维护";value="维护"},
    {text="离线";value="离线"}
];
for i,item in statusOptions {
    statusPicker.Items.Add(AntdUI.SelectItem(item.text,item.value));
}
statusPicker.SelectedIndex = 0;

var resetButton = AntdUI.Button(winform.resetHost);
resetButton.Type = AntdUI.TTypeMini.Default;

var summary = AntdUI.Alert(winform.summaryHost);
summary.TextTitle = "当前视图";
summary.Text = "准备加载数据";
summary.Icon = AntdUI.TType.Info;
summary.CloseIcon = false;
summary.Radius = 6;

var grid = AntdUI.Table(winform.tableHost);
grid.Bordered = true;
grid.EmptyText = "没有匹配的设备";
grid.RowHeight = 42;
grid.RowHeightHeader = 42;
grid.FixedHeader = true;
grid.EditMode = AntdUI.TEditMode.DoubleClick;

grid.Columns.Add(AntdUI.ColumnCheck("selected","选择"));
var colId = AntdUI.Column("id","编号");
colId.Width = "90";
grid.Columns.Add(colId);
var colName = AntdUI.Column("name","设备名称");
colName.Width = "180";
colName.SortOrder = true;
grid.Columns.Add(colName);
grid.Columns.Add(AntdUI.Column("status","状态"));
grid.Columns.Add(AntdUI.Column("progress","巡检进度"));
grid.Columns.Add(AntdUI.ColumnSwitch("enabled","启用"));
var colOps = AntdUI.Column("ops","操作");
colOps.Width = "100";
grid.Columns.Add(colOps);

var pager = AntdUI.Pagination(winform.pageHost);
pager.PageSize = 6;
pager.Current = 1;
pager.Total = 0;
pager.ShowSizeChanger = true;

// 业务数据永远保存在 aardio 数组中,Table 只显示当前页 DataTable。
var sourceItems = [
    {id=1001;name="东区网关 A";status="运行";progress=0.92;enabled=true;selected=false},
    {id=1002;name="西区传感器 B";status="维护";progress=0.35;enabled=true;selected=false},
    {id=1003;name="仓库温控 C";status="离线";progress=0.12;enabled=false;selected=false},
    {id=1004;name="主楼门禁 D";status="运行";progress=0.76;enabled=true;selected=false},
    {id=1005;name="机房 UPS E";status="维护";progress=0.58;enabled=true;selected=false},
    {id=1006;name="车库摄像头 F";status="运行";progress=0.88;enabled=true;selected=false},
    {id=1007;name="屋顶气象站 G";status="离线";progress=0.2;enabled=false;selected=false},
    {id=1008;name="南门访客机 H";status="运行";progress=0.67;enabled=true;selected=false}
];

var pageItems = [];
var isRefreshing = false;
var sortKey = "id";
var sortDesc = false;

var textOrEmpty = function(v){
    // aardio 中 0 与 false 为假,不能写 v || "",否则会吞掉 0/false。
    if(v === null) return "";
    return tostring(v);
}

var statusCell = function(text){
    select(text) {
        case "运行" {
            return AntdUI.CellTag("运行",AntdUI.TTypeMini.Success);
        }
        case "维护" {
            return AntdUI.CellTag("维护",AntdUI.TTypeMini.Warn);
        }
        case "离线" {
            return AntdUI.CellTag("离线",AntdUI.TTypeMini.Error);
        }
    }
    return AntdUI.CellTag(text,AntdUI.TTypeMini.Default);
}

var buildTable = function(items){
    var dt = System.Data.DataTable();

    // System.Object 列可以承载 CellTag、CellProgress、CellButton 等复杂单元格对象。
    dt.Columns.Add("selected",System.Boolean);
    dt.Columns.Add("id",System.Object);
    dt.Columns.Add("name",System.Object);
    dt.Columns.Add("status",System.Object);
    dt.Columns.Add("progress",System.Object);
    dt.Columns.Add("enabled",System.Boolean);
    dt.Columns.Add("ops",System.Object);

    for i,item in items {
        var row = dt.NewRow();
        row["selected"] = item.selected;
        row["id"] = item.id;
        row["name"] = item.name;
        row["status"] = statusCell(item.status);
        row["progress"] = AntdUI.CellProgress(item.progress); // 0 到 1,不是 0 到 100。
        row["enabled"] = item.enabled;
        row["ops"] = AntdUI.CellButton("detail","详情",AntdUI.TTypeMini.Primary);
        dt.Rows.Add(row);
    }
    return dt;
}

var getStatusFilter = function(){
    var idx = statusPicker.SelectedIndex;
    if(idx >= 0 && statusOptions[idx + 1]){
        return statusOptions[idx + 1].value;
    }
    return "";
}

var filterAndSort = function(){
    var q = query.Text;
    if(q === null) q = "";
    q = string.lower(string.trim(q));
    var statusFilter = getStatusFilter();
    var result = [];

    for i,item in sourceItems {
        var hit = true;
        if(#q){
            var hay = string.lower(item.name + " " + item.id);
            hit = string.indexOf(hay,q) !== null;
        }
        if(hit && #statusFilter){
            hit = item.status == statusFilter;
        }
        if(hit) table.push(result,item);
    }

    table.sort(result,function(next){
        var a = owner[sortKey];
        var b = next[sortKey];
        if(sortDesc) return tostring(a) > tostring(b);
        return tostring(a) < tostring(b);
    });
    return result;
}

var refreshData = function(resetPage){
    if(isRefreshing) return;
    isRefreshing = true;

    if(resetPage) pager.Current = 1;
    var filtered = filterAndSort();
    pager.Total = #filtered;
    var maxPage = math.max(1,math.ceil(#filtered / pager.PageSize));
    if(pager.Current > maxPage) pager.Current = maxPage;

    pageItems = [];
    if(#filtered){
        var startAt = (pager.Current - 1) * pager.PageSize + 1;
        var endAt = math.min(startAt + pager.PageSize - 1,#filtered);
        pageItems = table.slice(filtered,startAt,endAt);
    }
    grid.DataSource = buildTable(pageItems);
    summary.Text = "共 " + #filtered + " 台设备,当前第 " + pager.Current + " 页 / " + maxPage + " 页";

    isRefreshing = false;
}

var showMessage = function(text,typ){
    // grid.Parent 是 AntdUI.BaseForm,可作为 Message 的 owner。
    var cfg = AntdUI.Message.Config(grid.Parent,text,typ);
    cfg.AutoClose = 2;
    AntdUI.Message.open(cfg);
}

query.TextChanged = function(sender,e){
    // 在 .NET 回调中重建 DataSource,建议延后到消息循环,避免递归刷新或焦点闪动。
    winform.setTimeout(function(){
        refreshData(true);
    },0);
}

statusPicker.SelectedIndexChanged = function(sender,e){
    refreshData(true);
}

resetButton.Click = function(sender,e){
    query.Text = "";
    statusPicker.SelectedIndex = 0;
    refreshData(true);
}

pager.ValueChanged = function(sender,e){
    if(isRefreshing) return;
    refreshData(false);
}

grid.SortModeChanged = function(sender,e){

    if(e.Column){
        sortKey = e.Column.Key;
        sortDesc = e.SortMode === 2;
        winform.setTimeout(function(){
            refreshData(false);
        },0);
    }

     return true;
}

grid.CheckedChanged = function(sender,e){
    // e.RowIndex 是 .NET 行索引,从 0 开始;aardio 数组 pageItems 从 1 开始。
    var item = pageItems[e.RowIndex + 1];
    if(item && e.Column){
        if(e.Column.Key == "selected" || e.Column.Key == "enabled"){
            item[e.Column.Key] = e.Value;
        }
    }
}

grid.CellEndEdit = function(sender,e){
    var item = pageItems[e.RowIndex + 1];
    if(item && e.Column && e.Column.Key == "name"){
        item.name = textOrEmpty(e.Value);
        showMessage("已更新设备名称",AntdUI.TType.Success);
    }
    return true;
}

grid.CellButtonClick = function(sender,e){
    var item = pageItems[e.RowIndex + 1];
    if(item){
        showMessage("打开详情:" + item.id + " / " + item.name,AntdUI.TType.Info);
    }
}

refreshData(true);
winform.show();
win.loopMessage();

关键设计说明

DataTable 的 System.Object 列

DataTable 不只能放普通文本。只要把列类型设为 System.Object,字段值就可以放入 AntdUI 的复杂单元格对象,例如:

dt.Columns.Add("status",System.Object);
dt.Columns.Add("progress",System.Object);
dt.Columns.Add("ops",System.Object);

row["status"] = AntdUI.CellTag("运行",AntdUI.TTypeMini.Success);
row["progress"] = AntdUI.CellProgress(0.76);
row["ops"] = AntdUI.CellButton("detail","详情",AntdUI.TTypeMini.Primary);

这适合“主要数据来自 DataTable,但少数列需要标签、进度条、按钮”的业务表格。若某个单元格要放多个链接/按钮,AntItem[] 行模型会更灵活,详见 TableCell 表格单元格

Pagination 只负责 UI

Pagination 不会自动切换 Table.DataSource。推荐把 filterAndSort()refreshData() 这类函数写清楚:

Table 事件常用参数

事件 常用字段 说明
CellButtonClick e.Btne.Recorde.RowIndexe.Column 点击 CellButton / CellLink
CheckedChanged e.Valuee.Recorde.RowIndexe.Column 复选框列、开关列或可勾选单元格变化。
CellEndEdit e.Valuee.Recorde.RowIndexe.Column 编辑结束;通常返回 true 表示应用修改。
SortModeChanged e.SortModee.Column 列排序模式变化;建议在业务数组中排序后重建当前页。

注意:这些事件里的 RowIndex 是 .NET 0 基索引;如果要访问 aardio 数组,需要 pageItems[e.RowIndex + 1]

常见坑

Markdown 格式