四个用于结构化数据的全新 sgcHTML 组件:TreeGrid、PivotTable、ActivityFeed 和 Splitter | eSeGeCe 博客

四个用于结构化数据的全新 sgcHTML 组件:TreeGrid、PivotTable、ActivityFeed 和 Splitter

· 组件

sgcHTML 刚刚新增了 25 个组件,这是按主题介绍它们的系列文章中的第一篇,共五篇。我们从四个围绕结构化、动态数据构建的组件开始:用于层级行的 TreeGrid、用于分组聚合的 PivotTable、用于实时事件流的 ActivityFeed,以及经常需要容纳以上所有内容的可调整大小双面板布局组件 Splitter。下面的每一个组件都只是一个普通的 Delphi/C++Builder/.NET 类:设置属性、调用 HTML,即可得到 Bootstrap 5 标记。无需 JavaScript 框架,无需构建步骤。

TreeGrid — 可折叠的层级表格

TsgcHTMLComponent_TreeGrid 渲染一个行可以折叠和展开的 Bootstrap 表格,它由简单的 Id / ParentId 节点对构建而成 — 与你已经用于组织结构图、分类树或嵌套物料清单数据的形状完全相同。为每个可见字段声明一个 TsgcHTMLTreeGridColumn,然后添加节点;父子关系会自动为你绘制出树形结构和折叠箭头。

uses
  sgcHTML_Component_TreeGrid;

var
  oTreeGrid: TsgcHTMLComponent_TreeGrid;
  oColumn: TsgcHTMLTreeGridColumn;
begin
  oTreeGrid := TsgcHTMLComponent_TreeGrid.Create(nil);
  try
    oTreeGrid.TreeGridID := 'orgchart';
    oTreeGrid.Sortable := True;

    oColumn := oTreeGrid.Columns.Add;
    oColumn.Caption := 'Department';
    oColumn.FieldName := 'Name';

    oColumn := oTreeGrid.Columns.Add;
    oColumn.Caption := 'Headcount';
    oColumn.FieldName := 'Headcount';
    oColumn.Align := tgaRight;

    oTreeGrid.AddNode('eng', '', ['Engineering', '42']);
    oTreeGrid.AddNode('eng-be', 'eng', ['Backend', '18']);
    oTreeGrid.AddNode('eng-fe', 'eng', ['Frontend', '15']);

    WebModule.Response := oTreeGrid.HTML;   // hierarchical table + scoped CSS + toggle script
  finally
    oTreeGrid.Free;
  end;
end;

// Or bind it straight to a self-referencing dataset (Id / ParentId columns):
oTreeGrid.LoadFromDataSet(qryDepartments, 'Id', 'ParentId');

如果你的数据已经保存在一个自引用表中,可以完全跳过手动调用节点的步骤,直接把父字段名和子字段名交给 LoadFromDataSet — 它会遍历数据集,一次性构建出整棵树。

PivotTable — 分组、聚合、合计

TsgcHTMLComponent_PivotTable 接收扁平的行数据,生成一个带有行合计、列合计和总合计的分组聚合表格,也就是你原本需要在电子表格里手工制作的那种报表。你声明用于分组的 RowFieldsColumnFields、一个或多个用于聚合的 Measures(求和、平均值、计数、最小值、最大值),以及一个 DataFields 列表,告诉它实际要读取哪些源列。

uses
  sgcHTML_Component_PivotTable;

var
  oPivot: TsgcHTMLComponent_PivotTable;
  oMeasure: TsgcHTMLPivotMeasure;
begin
  oPivot := TsgcHTMLComponent_PivotTable.Create(nil);
  try
    oPivot.Caption := 'Revenue by Region and Quarter';

    oPivot.RowFields.Add.FieldName := 'Region';
    oPivot.ColumnFields.Add.FieldName := 'Quarter';

    oMeasure := oPivot.Measures.Add;
    oMeasure.SourceField := 'Amount';
    oMeasure.Caption := 'Revenue';
    oMeasure.Aggregation := paSum;
    oMeasure.Format := '#,##0.00';

    oPivot.DataFields.Add('Region');
    oPivot.DataFields.Add('Quarter');
    oPivot.DataFields.Add('Amount');

    oPivot.AddRow(['EMEA', 'Q1', '12000']);
    oPivot.AddRow(['EMEA', 'Q2', '15500']);
    oPivot.AddRow(['APAC', 'Q1', '9800']);

    WebModule.Response := oPivot.HTML;   // aggregated table + row/column/grand totals
  finally
    oPivot.Free;
  end;
end;

// Or bind it straight to a dataset (reads only RowFields/ColumnFields/Measures fields):
oPivot.LoadFromDataSet(qrySales);

AddRow(或 LoadFromDataSet)之后的一切都会为你自动计算:组件会对原始行进行分组,为每个单元格应用所选的聚合方式,并自动追加行合计、列合计和总合计的行与列。

ActivityFeed — 实时可推送的事件流

TsgcHTMLComponent_ActivityFeed 渲染一个按最新排在最前面的"谁做了什么"条目列表,并带有相对时间戳(如"3 分钟前",在客户端保持实时更新),而且它从设计之初就是为了通过 WebSocket 推送更新,而不仅仅是渲染一次。MaxItems 让它保持环形缓冲区的行为 — 一旦达到上限,最旧的条目会自动被移除。

uses
  sgcHTML_Enums, sgcHTML_Component_ActivityFeed;

var
  oFeed: TsgcHTMLComponent_ActivityFeed;
begin
  oFeed := TsgcHTMLComponent_ActivityFeed.Create(nil);
  try
    oFeed.FeedID := 'timeline';
    oFeed.Title := 'Recent Activity';
    oFeed.MaxItems := 30;

    oFeed.AddActivity('Ana', 'closed', 'ticket #482', hcSuccess, 'bi bi-check-circle');
    oFeed.AddActivity('Marc', 'commented on', 'invoice #1190', hcPrimary);

    WebModule.Response := oFeed.HTML;   // feed list + scoped CSS + live relative-time script
  finally
    oFeed.Free;
  end;
end;

// Push a new item live to connected clients (out-of-band htmx fragment):
oFeed.AddActivity('Ana', 'approved', 'purchase order #77', hcSuccess);
Engine.BroadcastFragment(oFeed.GetLastItemFragmentHTML);

最后一行才是关键所在:GetLastItemFragmentHTML 只把最新的这一条渲染成一个带外(out-of-band)htmx 片段,而 BroadcastFragment 会通过你已经在运行的 sgcWebSockets 服务器,把它推送给每一个打开的浏览器。不需要轮询,也不需要额外搭建一层实时通信。

Splitter — 承载一切的双面板外壳

TsgcHTMLComponent_Splitter 是布局组件:两个可调整大小的面板之间夹着一条可拖动的分隔条,可横向也可纵向,并为两侧各设置一个像素下限,使任何一个面板都不会被拖到消失。这正是 TreeGrid 加详情面板、或文件列表加编辑器这类界面所需要的外壳。

uses
  sgcHTML_Component_Splitter;

var
  oSplitter: TsgcHTMLComponent_Splitter;
begin
  oSplitter := TsgcHTMLComponent_Splitter.Create(nil);
  try
    oSplitter.SplitterID := 'workspace';
    oSplitter.Orientation := soHorizontal;
    oSplitter.InitialSplit := 30;
    oSplitter.MinSizeA := 180;
    oSplitter.MinSizeB := 320;
    oSplitter.GutterSize := 8;
    oSplitter.Height := '560px';
    oSplitter.PersistKey := 'workspace-split';

    oSplitter.AddPaneA('<div class="p-3"><h5>Files</h5>...</div>');
    oSplitter.AddPaneB('<div class="p-3"><h5>Editor</h5>...</div>');

    WebModule.Response := oSplitter.HTML;   // two panes + gutter + scoped CSS + drag script
  finally
    oSplitter.Free;
  end;
end;

PersistKey 值得特别一提:设置它之后,访问者拖动分隔条到的位置会在客户端被记住,并在下次访问时恢复,这样工作区布局就能跨会话保持不变,而你不需要编写任何状态管理代码。

立即体验

这四个组件都已包含在当前的 sgcWebSockets 版本中,各自的页面提供完整文档、关键属性参考以及 Delphi/C++Builder/.NET 代码示例:TreeGridPivotTableActivityFeedSplitter完整功能矩阵现在列出了九大类共 83 个组件 — 本系列的下一篇文章将介绍全新的图表和视觉代码组件。

有疑问、反馈,或需要迁移帮助?联系我们 — 回复你的将是编写这些代码的人。