huibo-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/cjs/hb-form-item.cjs.entry.js +59 -9
  2. package/dist/cjs/hb-form-item.cjs.entry.js.map +1 -1
  3. package/dist/cjs/hb-form.cjs.entry.js +105 -2
  4. package/dist/cjs/hb-form.cjs.entry.js.map +1 -1
  5. package/dist/cjs/hb-select.cjs.entry.js +82 -10
  6. package/dist/cjs/hb-select.cjs.entry.js.map +1 -1
  7. package/dist/cjs/hb-steps.cjs.entry.js +1 -1
  8. package/dist/cjs/hb-steps.cjs.entry.js.map +1 -1
  9. package/dist/cjs/hb-table.cjs.entry.js +195 -27
  10. package/dist/cjs/hb-table.cjs.entry.js.map +1 -1
  11. package/dist/cjs/huibo-ui.cjs.js +1 -1
  12. package/dist/cjs/loader.cjs.js +1 -1
  13. package/dist/collection/components/Form/Form.js +205 -2
  14. package/dist/collection/components/Form/Form.js.map +1 -1
  15. package/dist/collection/components/Form/FormItem.js +117 -10
  16. package/dist/collection/components/Form/FormItem.js.map +1 -1
  17. package/dist/collection/components/Select/Select.js +105 -10
  18. package/dist/collection/components/Select/Select.js.map +1 -1
  19. package/dist/collection/components/Table/Table.js +273 -27
  20. package/dist/collection/components/Table/Table.js.map +1 -1
  21. package/dist/collection/utils/virtual-scroll.js +39 -0
  22. package/dist/collection/utils/virtual-scroll.js.map +1 -0
  23. package/dist/components/hb-form-item.js +62 -9
  24. package/dist/components/hb-form-item.js.map +1 -1
  25. package/dist/components/hb-form.js +110 -2
  26. package/dist/components/hb-form.js.map +1 -1
  27. package/dist/components/hb-select.js +87 -11
  28. package/dist/components/hb-select.js.map +1 -1
  29. package/dist/components/hb-steps.js +1 -1
  30. package/dist/components/hb-steps.js.map +1 -1
  31. package/dist/components/hb-table.js +203 -29
  32. package/dist/components/hb-table.js.map +1 -1
  33. package/dist/esm/hb-form-item.entry.js +59 -9
  34. package/dist/esm/hb-form-item.entry.js.map +1 -1
  35. package/dist/esm/hb-form.entry.js +105 -2
  36. package/dist/esm/hb-form.entry.js.map +1 -1
  37. package/dist/esm/hb-select.entry.js +82 -10
  38. package/dist/esm/hb-select.entry.js.map +1 -1
  39. package/dist/esm/hb-steps.entry.js +1 -1
  40. package/dist/esm/hb-steps.entry.js.map +1 -1
  41. package/dist/esm/hb-table.entry.js +195 -27
  42. package/dist/esm/hb-table.entry.js.map +1 -1
  43. package/dist/esm/huibo-ui.js +1 -1
  44. package/dist/esm/loader.js +1 -1
  45. package/dist/huibo-ui/huibo-ui.esm.js +1 -1
  46. package/dist/huibo-ui/huibo-ui.esm.js.map +1 -1
  47. package/dist/huibo-ui/{p-79b24b83.entry.js → p-2cf5bf20.entry.js} +2 -2
  48. package/dist/huibo-ui/{p-79b24b83.entry.js.map → p-2cf5bf20.entry.js.map} +1 -1
  49. package/dist/huibo-ui/p-4148d875.entry.js +2 -0
  50. package/dist/huibo-ui/p-4148d875.entry.js.map +1 -0
  51. package/dist/huibo-ui/{p-54a28052.entry.js → p-6bfe1954.entry.js} +2 -2
  52. package/dist/huibo-ui/p-6bfe1954.entry.js.map +1 -0
  53. package/dist/huibo-ui/{p-ac18c68b.entry.js → p-e8824b2c.entry.js} +2 -2
  54. package/dist/huibo-ui/p-e8824b2c.entry.js.map +1 -0
  55. package/dist/huibo-ui/p-f69599fa.entry.js +2 -0
  56. package/dist/huibo-ui/p-f69599fa.entry.js.map +1 -0
  57. package/dist/types/components/Form/Form.d.ts +57 -0
  58. package/dist/types/components/Form/FormItem.d.ts +23 -0
  59. package/dist/types/components/Select/Select.d.ts +19 -0
  60. package/dist/types/components/Table/Table.d.ts +103 -8
  61. package/dist/types/components.d.ts +148 -2
  62. package/dist/types/utils/virtual-scroll.d.ts +38 -0
  63. package/package.json +1 -1
  64. package/dist/huibo-ui/p-29092b85.entry.js +0 -2
  65. package/dist/huibo-ui/p-29092b85.entry.js.map +0 -1
  66. package/dist/huibo-ui/p-2bc30b1b.entry.js +0 -2
  67. package/dist/huibo-ui/p-2bc30b1b.entry.js.map +0 -1
  68. package/dist/huibo-ui/p-54a28052.entry.js.map +0 -1
  69. package/dist/huibo-ui/p-ac18c68b.entry.js.map +0 -1
@@ -16,6 +16,8 @@ const Table = class {
16
16
  this.hbAction = index.createEvent(this, "hbAction", 7);
17
17
  this.hbExpandChange = index.createEvent(this, "hbExpandChange", 7);
18
18
  this.hbCellChange = index.createEvent(this, "hbCellChange", 7);
19
+ this.hbPageChange = index.createEvent(this, "hbPageChange", 7);
20
+ this.hbFilterChange = index.createEvent(this, "hbFilterChange", 7);
19
21
  }
20
22
  /** 数据源 */
21
23
  data = [];
@@ -55,6 +57,13 @@ const Table = class {
55
57
  treeProps;
56
58
  /** 是否开启行内编辑模式(默认关闭)。需列配置 column.editable=true 才对该列生效 */
57
59
  editable = false;
60
+ /**
61
+ * 内置分页配置(T1)。传入后 Table 自动对 data 切片并在底部渲染 <hb-pagination>。
62
+ * 不传则不分页(向后兼容)。
63
+ */
64
+ pagination;
65
+ /** 行 className(函数,按行返回附加 class,T8) */
66
+ rowClassName;
58
67
  sortProp = '';
59
68
  sortDirection = null;
60
69
  currentRow = -1;
@@ -65,6 +74,12 @@ const Table = class {
65
74
  expandedRowKeys = [];
66
75
  /** 行内编辑:当前正在编辑的单元格 { key, prop },null 表示无 */
67
76
  editingCell = null;
77
+ /** T1 分页:内部当前页(非受控时用) */
78
+ internalPage = 1;
79
+ /** T1 分页:内部每页条数(非受控时用) */
80
+ internalPageSize = 10;
81
+ /** T3 筛选:内部筛选状态 { prop: 选中的 value 列表 }(非受控时用) */
82
+ internalFilters = {};
68
83
  /** 排序变化事件 */
69
84
  hbSortChange;
70
85
  /** 行点击事件 */
@@ -77,10 +92,21 @@ const Table = class {
77
92
  hbExpandChange;
78
93
  /** 行内编辑提交事件 */
79
94
  hbCellChange;
95
+ /** T1 分页变化事件 { current, pageSize } */
96
+ hbPageChange;
97
+ /** T3 筛选变化事件 { prop, values } */
98
+ hbFilterChange;
80
99
  handleDataChange() {
81
100
  this.selectedRows = new Set();
82
101
  this.emitSelection();
83
102
  }
103
+ handlePaginationChange() {
104
+ // 受控分页:从 pagination.current/pageSize 初始化内部状态
105
+ if (this.pagination) {
106
+ this.internalPage = this.pagination.current ?? this.pagination.defaultCurrent ?? 1;
107
+ this.internalPageSize = this.pagination.pageSize ?? this.pagination.defaultPageSize ?? 10;
108
+ }
109
+ }
84
110
  /** 树形模式是否启用(显式传入 treeProps 即启用) */
85
111
  get isTreeMode() {
86
112
  return !!this.treeProps;
@@ -91,9 +117,6 @@ const Table = class {
91
117
  }
92
118
  /**
93
119
  * 把树拍平成可见行列表:仅保留「祖先链路全部已展开」的后代。
94
- * @param list 当前层级的数组
95
- * @param level 当前层级(根 = 0)
96
- * @param parentKeyPath 父级 key 路径,用于构造深层稳定 key(避免不同分支同 index 撞 key)
97
120
  */
98
121
  flattenTree(list, level = 0, parentKeyPath = '') {
99
122
  const field = this.childrenField;
@@ -104,7 +127,6 @@ const Table = class {
104
127
  const kids = row[field];
105
128
  const hasKids = Array.isArray(kids) && kids.length > 0;
106
129
  result.push({ row, level, key, hasChildren: hasKids, index: i });
107
- // 仅当该行已展开时,递归拍入其 children
108
130
  if (hasKids && this.expandedRowKeys.indexOf(key) !== -1) {
109
131
  result.push(...this.flattenTree(kids, level + 1, keyPath));
110
132
  }
@@ -129,7 +151,6 @@ const Table = class {
129
151
  commitEdit = (e, row, prop) => {
130
152
  const input = e.target;
131
153
  const value = input.value;
132
- // 就地更新(简单可用版):修改 row[prop] 后强制重渲染
133
154
  row[prop] = value;
134
155
  this.editingCell = null;
135
156
  this.hbCellChange.emit({ row, prop, value });
@@ -147,7 +168,6 @@ const Table = class {
147
168
  };
148
169
  allCheckboxRef;
149
170
  componentDidRender() {
150
- // indeterminate 不是可反射的 HTML 属性,必须通过 DOM 属性设置
151
171
  if (this.allCheckboxRef) {
152
172
  this.allCheckboxRef.indeterminate = this.isIndeterminate;
153
173
  }
@@ -178,7 +198,24 @@ const Table = class {
178
198
  get virtualTotalHeight() {
179
199
  return this.data.length * (this.itemHeight > 0 ? this.itemHeight : 1);
180
200
  }
181
- handleSort = (prop) => {
201
+ /**
202
+ * T2:解析某列的有效排序方向。
203
+ * 受控(col.sortOrder 设置)→ 用该值;否则用内部 state(sortProp/sortDirection)。
204
+ */
205
+ effectiveSortOrder(col) {
206
+ if (col.sortOrder !== undefined)
207
+ return col.sortOrder;
208
+ if (this.sortProp === col.prop)
209
+ return this.sortDirection;
210
+ return null;
211
+ }
212
+ handleSort = (col) => {
213
+ const prop = col.prop;
214
+ // T2:受控排序(col.sortOrder 已定义)→ 不内部切换,仅 emit 交外部决策
215
+ if (col.sortOrder !== undefined) {
216
+ this.hbSortChange.emit({ prop, order: col.sortOrder });
217
+ return;
218
+ }
182
219
  if (this.sortProp === prop) {
183
220
  this.sortDirection = this.sortDirection === 'ascending' ? 'descending' : this.sortDirection === 'descending' ? null : 'ascending';
184
221
  }
@@ -188,13 +225,38 @@ const Table = class {
188
225
  }
189
226
  this.hbSortChange.emit({ prop, order: this.sortDirection });
190
227
  };
228
+ /**
229
+ * T2 增强的排序数据:支持 col.sorter 自定义排序函数 + 受控 sortOrder。
230
+ * 多列排序不支持(与 antd 行为一致,取首个有效排序列)。
231
+ */
191
232
  get sortedData() {
192
- if (!this.sortProp || !this.sortDirection)
233
+ // 找到当前生效的排序列
234
+ let activeCol;
235
+ let activeOrder = null;
236
+ for (const col of this.columns) {
237
+ if (col.sortable || col.sorter) {
238
+ const order = this.effectiveSortOrder(col);
239
+ if (order) {
240
+ activeCol = col;
241
+ activeOrder = order;
242
+ break;
243
+ }
244
+ }
245
+ }
246
+ // 兼容旧逻辑:若没有列级 sortOrder,回退到内部 sortProp/sortDirection
247
+ if (!activeCol && this.sortProp && this.sortDirection) {
248
+ activeOrder = this.sortDirection;
249
+ }
250
+ if (!activeOrder)
193
251
  return this.data;
194
- const direction = this.sortDirection === 'ascending' ? 1 : -1;
252
+ const direction = activeOrder === 'ascending' ? 1 : -1;
253
+ const sortProp = activeCol ? activeCol.prop : this.sortProp;
254
+ const sorterFn = activeCol && activeCol.sorter;
195
255
  return [...this.data].sort((a, b) => {
196
- const va = a[this.sortProp];
197
- const vb = b[this.sortProp];
256
+ if (sorterFn)
257
+ return sorterFn(a, b) * direction;
258
+ const va = a[sortProp];
259
+ const vb = b[sortProp];
198
260
  if (va < vb)
199
261
  return -1 * direction;
200
262
  if (va > vb)
@@ -202,6 +264,72 @@ const Table = class {
202
264
  return 0;
203
265
  });
204
266
  }
267
+ /**
268
+ * T3 筛选:应用所有列的 onFilter。
269
+ * 受控(col.filteredValue 已定义)→ 用该值;否则用内部 internalFilters[prop]。
270
+ */
271
+ get filteredData() {
272
+ let result = this.sortedData;
273
+ for (const col of this.columns) {
274
+ if (!col.onFilter || !col.filters)
275
+ continue;
276
+ const values = col.filteredValue !== undefined ? col.filteredValue : this.internalFilters[col.prop];
277
+ if (!values || values.length === 0)
278
+ continue;
279
+ result = result.filter(row => values.some(v => col.onFilter(row, v)));
280
+ }
281
+ return result;
282
+ }
283
+ /**
284
+ * T1 分页:取当前页数据切片(仅当 pagination 配置时)。
285
+ * 在 sorted+filtered 之后切片。
286
+ */
287
+ get pagedData() {
288
+ const filtered = this.filteredData;
289
+ if (!this.pagination)
290
+ return filtered;
291
+ const page = this.currentPage;
292
+ const size = this.currentPageSize;
293
+ const start = (page - 1) * size;
294
+ return filtered.slice(start, start + size);
295
+ }
296
+ /** T1:当前页(受控优先) */
297
+ get currentPage() {
298
+ return this.pagination?.current ?? this.internalPage;
299
+ }
300
+ /** T1:当前每页条数(受控优先) */
301
+ get currentPageSize() {
302
+ return this.pagination?.pageSize ?? this.internalPageSize;
303
+ }
304
+ /** T1:分页切换 */
305
+ handlePageChange = (page) => {
306
+ this.internalPage = page;
307
+ this.hbPageChange.emit({ current: page, pageSize: this.currentPageSize });
308
+ };
309
+ /** T1:每页条数切换 */
310
+ handlePageSizeChange = (size) => {
311
+ this.internalPageSize = size;
312
+ // 切条数后回到第一页避免越界
313
+ this.internalPage = 1;
314
+ this.hbPageChange.emit({ current: 1, pageSize: size });
315
+ };
316
+ /** T3:筛选下拉切换某项 */
317
+ handleFilterToggle = (col, value) => {
318
+ const current = col.filteredValue !== undefined ? col.filteredValue : this.internalFilters[col.prop] || [];
319
+ const next = current.includes(value) ? current.filter(v => v !== value) : [...current, value];
320
+ // 仅内部状态时更新;受控(filteredValue 已定义)则只 emit 交外部
321
+ if (col.filteredValue === undefined) {
322
+ this.internalFilters = { ...this.internalFilters, [col.prop]: next };
323
+ }
324
+ this.hbFilterChange.emit({ prop: col.prop, values: next });
325
+ };
326
+ /** T3:清空某列筛选 */
327
+ handleFilterReset = (col) => {
328
+ if (col.filteredValue === undefined) {
329
+ this.internalFilters = { ...this.internalFilters, [col.prop]: [] };
330
+ }
331
+ this.hbFilterChange.emit({ prop: col.prop, values: [] });
332
+ };
205
333
  get isAllSelected() {
206
334
  return this.data.length > 0 && this.selectedRows.size === this.data.length;
207
335
  }
@@ -246,10 +374,14 @@ const Table = class {
246
374
  get hasActions() {
247
375
  return !!this.actions && this.actions.length > 0;
248
376
  }
377
+ /** T3:某列是否有激活筛选 */
378
+ isColumnFiltered(col) {
379
+ const values = col.filteredValue !== undefined ? col.filteredValue : this.internalFilters[col.prop];
380
+ return !!values && values.length > 0;
381
+ }
249
382
  /**
250
383
  * 计算固定列的 sticky 偏移(左侧/右侧累计宽度)。
251
384
  * P1 优化:缓存结果,仅当 columns 引用变化时重算。
252
- * 修复前是 getter,被每个 <td>/<th> 反复调用,复杂度 O(rows×cols²)。
253
385
  */
254
386
  _fixedOffsetsCache = null;
255
387
  _fixedOffsetsColumnsRef = null;
@@ -293,11 +425,18 @@ const Table = class {
293
425
  }
294
426
  return style;
295
427
  }
428
+ /**
429
+ * 渲染单元格内容(T7:优先 col.render,其次 formatter,最后原值)。
430
+ */
431
+ renderCell(col, row, rowIndex) {
432
+ if (col.render)
433
+ return col.render(row, rowIndex);
434
+ if (col.formatter)
435
+ return col.formatter(row, rowIndex);
436
+ return row[col.prop] ?? '';
437
+ }
296
438
  /**
297
439
  * 渲染单行(普通 + 虚拟滚动 + 树形 共用)。
298
- * - 普通模式:rowIndex 为在 sortedData 中的全局索引。
299
- * - 树形模式:fr.level 决定缩进,第一列渲染展开箭头,key 取 fr.key。
300
- * fr 为可选;未传时按普通模式渲染。
301
440
  */
302
441
  renderRow(row, rowIndex, fr) {
303
442
  const tree = !!fr;
@@ -305,15 +444,17 @@ const Table = class {
305
444
  const rowKeyVal = fr ? fr.key : this.rowKey ? row[this.rowKey] : rowIndex;
306
445
  const indent = level * 16;
307
446
  const isEditing = (prop) => !!this.editingCell && this.editingCell.rowKey === rowKeyVal && this.editingCell.prop === prop;
447
+ // T8:行 className
448
+ const extraRowClass = this.rowClassName ? this.rowClassName(row, rowIndex) : '';
308
449
  return (index.h("tr", { key: rowKeyVal, class: {
309
450
  'hb-table__row': true,
310
451
  'hb-table__row--striped': this.stripe && rowIndex % 2 === 1,
311
452
  'hb-table__row--current': this.highlightCurrentRow && rowIndex === this.currentRow,
312
453
  'hb-table__row--selected': this.selectedRows.has(rowIndex),
313
454
  'hb-table__row--tree': tree,
455
+ [extraRowClass]: !!extraRowClass,
314
456
  }, onClick: () => this.handleRowClick(row, rowIndex) }, this.selectable && (index.h("td", { class: "hb-table__selection", onClick: e => e.stopPropagation() }, index.h("input", { type: "checkbox", class: "hb-table__checkbox", checked: this.selectedRows.has(rowIndex), onChange: e => this.toggleRow(rowIndex, e) }))), this.columns.map((col, colIdx) => {
315
457
  const off = this.getFixedOffsets()[col.prop];
316
- // 树形模式:第一列加缩进 + 展开箭头
317
458
  const isFirstCol = colIdx === 0;
318
459
  const showExpand = tree && isFirstCol && fr.hasChildren;
319
460
  const expanded = showExpand && this.expandedRowKeys.indexOf(rowKeyVal) !== -1;
@@ -324,52 +465,79 @@ const Table = class {
324
465
  'hb-table__cell--fixed-left': off?.side === 'left',
325
466
  'hb-table__cell--fixed-right': off?.side === 'right',
326
467
  'hb-table__cell--editable': editableActive,
468
+ 'hb-table__cell--ellipsis': !!col.ellipsis,
327
469
  }, onClick: editableActive ? e => this.startEdit(e, rowKeyVal, col.prop) : undefined }, tree && isFirstCol && index.h("span", { class: "hb-table__indent", style: { display: 'inline-block', width: `${indent}px` }, "aria-hidden": "true" }), showExpand && (index.h("button", { type: "button", class: { 'hb-table__expand': true, 'hb-table__expand--expanded': expanded }, "aria-label": expanded ? '收起' : '展开', onClick: e => {
328
470
  e.stopPropagation();
329
471
  this.toggleExpand(row, rowKeyVal);
330
- } }, expanded ? '▼' : '▶')), showExpand === false && tree && isFirstCol && index.h("span", { class: "hb-table__expand-placeholder", style: { display: 'inline-block', width: '16px' }, "aria-hidden": "true" }), editing ? (index.h("input", { class: "hb-table__edit-input", type: "text", value: row[col.prop] ?? '', onBlur: e => this.commitEdit(e, row, col.prop), onKeyDown: e => this.onEditKeydown(e, row, col.prop) })) : (index.h("slot", { name: `cell-${col.prop}` }, col.formatter ? col.formatter(row, rowIndex) : (row[col.prop] ?? '')))));
472
+ } }, expanded ? '▼' : '▶')), showExpand === false && tree && isFirstCol && index.h("span", { class: "hb-table__expand-placeholder", style: { display: 'inline-block', width: '16px' }, "aria-hidden": "true" }), editing ? (index.h("input", { class: "hb-table__edit-input", type: "text", value: row[col.prop] ?? '', onBlur: e => this.commitEdit(e, row, col.prop), onKeyDown: e => this.onEditKeydown(e, row, col.prop) })) : (
473
+ // T7:col.render 优先(支持返回 JSX);否则 formatter;否则原值
474
+ index.h("slot", { name: `cell-${col.prop}` }, this.renderCell(col, row, rowIndex)))));
331
475
  }), this.hasActions && (index.h("td", { class: "hb-table__td--actions", style: { textAlign: 'right', whiteSpace: 'nowrap' }, onClick: e => e.stopPropagation() }, this.actions.map(act => (index.h("button", { type: "button", class: {
332
476
  'hb-table__action': true,
333
477
  'hb-table__action--primary': act.type === 'primary',
334
478
  'hb-table__action--danger': act.type === 'danger',
335
479
  }, disabled: act.disabled ? act.disabled(row, rowIndex) : false, onClick: () => this.handleAction(row, rowIndex, act.key) }, act.label)))))));
336
480
  }
481
+ /**
482
+ * T3:渲染表头某列的筛选下拉。
483
+ */
484
+ renderColumnFilter = (col) => {
485
+ if (!col.filters || col.filters.length === 0 || !col.onFilter)
486
+ return null;
487
+ const activeValues = col.filteredValue !== undefined ? col.filteredValue : this.internalFilters[col.prop] || [];
488
+ const filtered = this.isColumnFiltered(col);
489
+ return (index.h("span", { class: "hb-table__filter", onClick: e => e.stopPropagation() }, index.h("details", { class: "hb-table__filter-dropdown" }, index.h("summary", { class: { 'hb-table__filter-trigger': true, 'hb-table__filter-trigger--active': filtered }, "aria-label": "\u7B5B\u9009" }, index.h("svg", { viewBox: "0 0 24 24", width: "14", height: "14", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, index.h("path", { d: "M22 3H2l8 9.46V19l4 2v-8.54L22 3z" }))), index.h("div", { class: "hb-table__filter-menu", role: "menu" }, index.h("ul", { class: "hb-table__filter-list" }, col.filters.map(f => {
490
+ const checked = activeValues.includes(f.value);
491
+ return (index.h("li", { class: "hb-table__filter-item", role: "menuitemcheckbox", "aria-checked": checked ? 'true' : 'false' }, index.h("label", { class: "hb-table__filter-label" }, index.h("input", { type: "checkbox", class: "hb-table__filter-checkbox", checked: checked, onChange: () => this.handleFilterToggle(col, f.value) }), index.h("span", null, f.label))));
492
+ })), index.h("div", { class: "hb-table__filter-actions" }, index.h("button", { type: "button", class: "hb-table__filter-reset", onClick: () => this.handleFilterReset(col) }, "\u91CD\u7F6E"))))));
493
+ };
337
494
  /** 渲染表头(普通 + 虚拟滚动共用,表头始终固定不随滚) */
338
495
  renderHeader() {
339
496
  return (index.h("thead", null, index.h("tr", null, this.selectable && (index.h("th", { class: "hb-table__selection" }, index.h("input", { type: "checkbox", class: "hb-table__checkbox", checked: this.isAllSelected, ref: el => {
340
497
  this.allCheckboxRef = el;
341
498
  }, onChange: this.toggleAll }))), this.columns.map(col => {
342
499
  const off = this.getFixedOffsets()[col.prop];
500
+ // T2:列可排序的条件(sortable 或 sorter)
501
+ const canSort = !!col.sortable || !!col.sorter;
502
+ const order = this.effectiveSortOrder(col);
343
503
  return (index.h("th", { key: col.prop, style: this.cellStyle(col.prop, {
344
504
  width: col.width,
345
505
  minWidth: col.minWidth,
346
506
  textAlign: col.align || 'left',
347
507
  }), class: {
348
- 'hb-table__th--sortable': col.sortable,
508
+ 'hb-table__th--sortable': canSort,
349
509
  'hb-table__cell--fixed-left': off?.side === 'left',
350
510
  'hb-table__cell--fixed-right': off?.side === 'right',
351
- }, onClick: col.sortable ? () => this.handleSort(col.prop) : undefined }, index.h("div", { class: "hb-table__th-content" }, index.h("span", null, col.label), col.sortable && (index.h("span", { class: "hb-table__sort" }, index.h("span", { class: { 'hb-table__sort-icon': true, 'hb-table__sort-icon--active': this.sortProp === col.prop && this.sortDirection === 'ascending' } }, "\u25B2"), index.h("span", { class: { 'hb-table__sort-icon': true, 'hb-table__sort-icon--active': this.sortProp === col.prop && this.sortDirection === 'descending' } }, "\u25BC"))))));
511
+ }, onClick: canSort ? () => this.handleSort(col) : undefined }, index.h("div", { class: "hb-table__th-content" }, index.h("span", null, col.label), canSort && (index.h("span", { class: "hb-table__sort" }, index.h("span", { class: { 'hb-table__sort-icon': true, 'hb-table__sort-icon--active': order === 'ascending' } }, "\u25B2"), index.h("span", { class: { 'hb-table__sort-icon': true, 'hb-table__sort-icon--active': order === 'descending' } }, "\u25BC"))), this.renderColumnFilter(col))));
352
512
  }), this.hasActions && (index.h("th", { class: "hb-table__th--actions", style: { textAlign: 'right', whiteSpace: 'nowrap' } }, "\u64CD\u4F5C")))));
353
513
  }
514
+ /** T1:渲染内置分页底部栏 */
515
+ renderPagination() {
516
+ if (!this.pagination)
517
+ return null;
518
+ const total = this.filteredData.length;
519
+ const page = this.currentPage;
520
+ const size = this.currentPageSize;
521
+ const totalPages = Math.max(1, Math.ceil(total / size));
522
+ const showTotal = this.pagination.showTotal;
523
+ return (index.h("div", { class: "hb-table__pagination" }, showTotal && index.h("span", { class: "hb-table__pagination-total" }, "\u5171 ", total, " \u6761"), index.h("span", { class: "hb-table__pagination-pages" }, index.h("button", { type: "button", class: "hb-table__pagination-btn", disabled: page <= 1, onClick: () => this.handlePageChange(page - 1), "aria-label": "\u4E0A\u4E00\u9875" }, "\u2039"), index.h("span", { class: "hb-table__pagination-current" }, page, " / ", totalPages), index.h("button", { type: "button", class: "hb-table__pagination-btn", disabled: page >= totalPages, onClick: () => this.handlePageChange(page + 1), "aria-label": "\u4E0B\u4E00\u9875" }, "\u203A")), this.pagination.pageSizes && (index.h("select", { class: "hb-table__pagination-size", onChange: e => this.handlePageSizeChange(parseInt(e.target.value, 10)) }, this.pagination.pageSizes.map(s => (index.h("option", { value: String(s), selected: s === size }, s, " \u6761/\u9875")))))));
524
+ }
354
525
  render() {
355
- const displayData = this.sortedData;
526
+ // T1+T2+T3:排序 筛选 → 分页 依次应用
527
+ const displayData = this.pagedData;
356
528
  const isEmpty = displayData.length === 0;
357
- // 树形模式:把可见树拍平成带 level 的行列表(仅含已展开链路上的后代)
358
529
  const flatRows = this.isTreeMode ? this.flattenTree(displayData) : [];
359
- // 虚拟滚动:固定高度滚动视口 + 撑高容器 + 仅渲染可见窗口行(绝对定位 offset)
360
- // 注意:树形 + 虚拟滚动组合较复杂,此处虚拟滚动沿用普通行渲染(不展开树),
361
- // 保留树形仅在普通模式下生效以降低风险。
362
530
  const renderVirtualBody = () => {
363
531
  const { startIndex, endIndex } = this.visibleRange;
364
532
  const offsetTop = startIndex * (this.itemHeight > 0 ? this.itemHeight : 1);
365
533
  return (index.h("div", { class: "hb-table__virtual-viewport", style: { height: `${this.virtualScrollHeight}px`, overflowY: 'auto' }, onScroll: this.handleVirtualScroll }, index.h("div", { class: "hb-table__virtual-spacer", style: { height: `${this.virtualTotalHeight}px`, position: 'relative' } }, index.h("table", { class: "hb-table__table hb-table__table--virtual" }, index.h("tbody", null, isEmpty ? (index.h("tr", null, index.h("td", { colSpan: this.columns.length + (this.selectable ? 1 : 0) + (this.hasActions ? 1 : 0), class: "hb-table__empty" }, this.emptyText))) : (index.h("tr", { style: { height: `${offsetTop}px` }, "aria-hidden": "true" })), displayData.slice(startIndex, endIndex).map((row, i) => this.renderRow(row, startIndex + i)))))));
366
534
  };
367
- // 普通模式:树形启用时按拍平列表渲染(带 level 缩进 + 展开箭头),否则原样按行渲染
368
535
  const renderNormalBody = () => (index.h("tbody", null, isEmpty ? (index.h("tr", null, index.h("td", { colSpan: this.columns.length + (this.selectable ? 1 : 0) + (this.hasActions ? 1 : 0), class: "hb-table__empty" }, this.emptyText))) : this.isTreeMode ? (flatRows.map((fr, i) => this.renderRow(fr.row, i, fr))) : (displayData.map((row, rowIndex) => this.renderRow(row, rowIndex)))));
369
- return (index.h("div", { class: { 'hb-table': true, [`hb-table--${this.size}`]: true, 'hb-table--border': this.border, 'hb-table--virtual': this.virtualScroll } }, this.loading && (index.h("div", { class: "hb-table__loading" }, index.h("div", { class: "hb-table__loading-spinner" }))), this.virtualScroll ? (index.h("div", { class: "hb-table__virtual-wrapper" }, index.h("table", { class: "hb-table__table" }, this.renderHeader()), renderVirtualBody())) : (index.h("div", { class: "hb-table__wrapper", style: this.maxHeight ? { maxHeight: this.maxHeight, overflow: 'auto' } : undefined }, index.h("table", { class: "hb-table__table" }, this.renderHeader(), renderNormalBody())))));
536
+ return (index.h("div", { class: { 'hb-table': true, [`hb-table--${this.size}`]: true, 'hb-table--border': this.border, 'hb-table--virtual': this.virtualScroll, 'hb-table--has-pagination': !!this.pagination } }, this.loading && (index.h("div", { class: "hb-table__loading" }, index.h("div", { class: "hb-table__loading-spinner" }))), this.virtualScroll ? (index.h("div", { class: "hb-table__virtual-wrapper" }, index.h("table", { class: "hb-table__table" }, this.renderHeader()), renderVirtualBody())) : (index.h("div", { class: "hb-table__wrapper", style: this.maxHeight ? { maxHeight: this.maxHeight, overflow: 'auto' } : undefined }, index.h("table", { class: "hb-table__table" }, this.renderHeader(), renderNormalBody()))), this.renderPagination()));
370
537
  }
371
538
  static get watchers() { return {
372
- "data": ["handleDataChange"]
539
+ "data": ["handleDataChange"],
540
+ "pagination": ["handlePaginationChange"]
373
541
  }; }
374
542
  };
375
543
  Table.style = HbTableStyle0;