@ticatec/topside-elements 1.0.6 → 1.2.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 (42) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/app_layout/ClassicLayout.svelte +22 -11
  3. package/dist/app_layout/ClassicLayout.svelte.d.ts +6 -0
  4. package/dist/app_layout/ColumnsLayout.svelte +7 -4
  5. package/dist/app_layout/ColumnsLayout.svelte.d.ts +2 -0
  6. package/dist/app_layout/HeaderLayout.svelte +11 -2
  7. package/dist/app_layout/HeaderLayout.svelte.d.ts +4 -0
  8. package/dist/app_layout/README.md +23 -1
  9. package/dist/app_layout/README_CN.md +23 -1
  10. package/dist/app_layout/RowsLayout.svelte +7 -4
  11. package/dist/app_layout/RowsLayout.svelte.d.ts +2 -0
  12. package/dist/app_layout/SidebarLayout.svelte +11 -2
  13. package/dist/app_layout/SidebarLayout.svelte.d.ts +4 -0
  14. package/dist/data_table/DataTable.svelte +153 -12
  15. package/dist/data_table/DataTable.svelte.d.ts +22 -2
  16. package/dist/data_table/README.md +119 -38
  17. package/dist/data_table/README_CN.md +119 -38
  18. package/dist/data_table/index.d.ts +5 -0
  19. package/dist/data_table/index.js +2 -0
  20. package/dist/data_table/lib/IndicatorColumn.d.ts +2 -1
  21. package/dist/data_table/lib/RowExpansion.d.ts +84 -0
  22. package/dist/data_table/lib/RowExpansion.js +43 -0
  23. package/dist/data_table/parts/ActionsPanel.svelte +4 -0
  24. package/dist/data_table/parts/ActionsPanel.svelte.d.ts +2 -0
  25. package/dist/data_table/parts/ActionsRow.svelte +8 -1
  26. package/dist/data_table/parts/ActionsRow.svelte.d.ts +2 -0
  27. package/dist/data_table/parts/ContentPanel.svelte +9 -1
  28. package/dist/data_table/parts/ContentPanel.svelte.d.ts +4 -0
  29. package/dist/data_table/parts/DataRow.svelte +13 -2
  30. package/dist/data_table/parts/DataRow.svelte.d.ts +2 -0
  31. package/dist/data_table/parts/FixedColumnsPanel.svelte +11 -2
  32. package/dist/data_table/parts/FixedColumnsPanel.svelte.d.ts +6 -1
  33. package/dist/data_table/parts/FixedRow.svelte +19 -2
  34. package/dist/data_table/parts/FixedRow.svelte.d.ts +4 -0
  35. package/docs/components/README.md +6 -6
  36. package/docs/components/classic-layout/README.md +3 -0
  37. package/docs/components/columns-layout/README.md +1 -0
  38. package/docs/components/data-table/README.md +53 -1
  39. package/docs/components/header-layout/README.md +2 -0
  40. package/docs/components/rows-layout/README.md +1 -0
  41. package/docs/components/sidebar-layout/README.md +2 -0
  42. package/package.json +1 -1
@@ -15,6 +15,13 @@
15
15
  import { buildTableSummary } from "./lib/summary";
16
16
  import { isSameColumn } from "./lib/TableUtils";
17
17
  import i18nRes from "../i18nRes";
18
+ import type RowExpansion from "./lib/RowExpansion";
19
+ import {
20
+ type RowExpandContext,
21
+ type RowExpandRequest,
22
+ type RowEventHandlers,
23
+ isInteractiveTarget
24
+ } from "./lib/RowExpansion";
18
25
 
19
26
  interface Props {
20
27
  columns: Array<DataColumn>;
@@ -45,6 +52,13 @@
45
52
  * 分页场景下用它显示服务端算好的全量合计(数字串会按数字格式化)。
46
53
  */
47
54
  summaryValues?: Record<string, any> | null;
55
+ /**
56
+ * 行展开配置:展开区组件、是否显示指示列图标(默认显示)、整行单击 / 双击触发等。
57
+ * 不配置图标时可以用组件方法 expandRow / collapseRow / toggleRowExpand,
58
+ * 或在行内(自定义单元格、操作列、展开区)派发 ROW_EXPAND_EVENT 事件来控制。
59
+ * 同一时间只展开一行。
60
+ */
61
+ rowExpansion?: RowExpansion<T> | null;
48
62
  }
49
63
 
50
64
  let {
@@ -65,6 +79,7 @@
65
79
  overscan = 8,
66
80
  summaryLabel,
67
81
  summaryValues = null,
82
+ rowExpansion = null,
68
83
  }: Props = $props();
69
84
 
70
85
  const id: string = Math.round(new Date().getTime() + Math.random() * 1000).toString(36);
@@ -78,13 +93,24 @@
78
93
  let frozenWidth = $state(0);
79
94
  let styleEl: HTMLStyleElement | undefined;
80
95
 
81
- let expandRow = $state<TableRow | null>(null);
96
+ let expandedRow = $state<TableRow | null>(null);
82
97
  let scrollTop = $state(0);
83
98
  let inlineComponent = $state<any>(null);
84
99
 
100
+ // 有效的行展开配置:rowExpansion 优先,兼容旧的 indicatorColumn.buildInlineComponent
101
+ let expansion = $derived.by<RowExpansion<T> | null>(() => {
102
+ if (rowExpansion) return rowExpansion;
103
+ const legacy = indicatorColumn?.buildInlineComponent;
104
+ return legacy ? { build: legacy } : null;
105
+ });
106
+ let expandable = $derived(expansion != null);
107
+ // 指示列图标:默认显示;需要有 indicatorColumn 才有地方放
108
+ let showExpandToggle = $derived(expandable && indicatorColumn != null && expansion?.showToggle !== false);
109
+ let expandTrigger = $derived(expansion?.trigger ?? null);
110
+
85
111
  // Measured by ContentPanel (its scrollable data area); drives the virtual window.
86
112
  let viewportHeight = $state(0);
87
- let expandIndex = $derived(expandRow ? rows.indexOf(expandRow) : -1);
113
+ let expandIndex = $derived(expandedRow ? rows.indexOf(expandedRow) : -1);
88
114
  // One shared window for all three panels (frozen / content / actions), so the
89
115
  // slices they render always cover the same absolute row indices.
90
116
  let vwindow = $derived(virtual
@@ -163,7 +189,7 @@
163
189
 
164
190
  const sortRows = (compareFun: CompareFn, descending: boolean) => {
165
191
  if (!table) return;
166
- expandRow = null;
192
+ closeExpanded();
167
193
  let oList = list.sort(compareFun);
168
194
  table.data = descending ? oList.reverse() : oList;
169
195
  rows = table.rows;
@@ -183,15 +209,125 @@
183
209
  }
184
210
  };
185
211
 
186
- const onRowExpand = async (row: TableRow) => {
187
- if (row == expandRow) {
188
- expandRow = null;
189
- inlineComponent = null;
212
+ const isExpandableRow = (row: TableRow): boolean =>
213
+ expandable && (expansion?.isExpandable?.(row.data) ?? true);
214
+
215
+ const closeExpanded = () => {
216
+ if (expandedRow == null) return;
217
+ expandedRow = null;
218
+ inlineComponent = null;
219
+ expansion?.onExpandChange?.(null);
220
+ };
221
+
222
+ // 异步 build 期间用户可能又点了别的行:只接受最后一次请求的结果
223
+ let expandSeq = 0;
224
+ const openRow = async (row: TableRow) => {
225
+ if (!isExpandableRow(row)) return;
226
+ const seq = ++expandSeq;
227
+ const comp = await expansion!.build(row.data);
228
+ if (seq !== expandSeq) return;
229
+ inlineComponent = comp;
230
+ expandedRow = row;
231
+ expansion?.onExpandChange?.(row.data);
232
+ };
233
+
234
+ /** 切换某行展开状态;expanded 指定目标状态,不传则切换 */
235
+ const setRowExpanded = async (row: TableRow, expanded?: boolean) => {
236
+ const isOpen = row === expandedRow;
237
+ const target = expanded ?? !isOpen;
238
+ if (target === isOpen) return;
239
+ if (target) {
240
+ await openRow(row);
190
241
  } else {
191
- inlineComponent = await indicatorColumn?.buildInlineComponent?.(row.data);
192
- expandRow = row;
242
+ expandSeq++;
243
+ closeExpanded();
193
244
  }
194
245
  };
246
+
247
+ const onRowExpand = (row: TableRow) => setRowExpanded(row);
248
+
249
+ // 对外方法(bind:this 后调用)用它定位行:按数据对象同一性匹配
250
+ const findRow = (data: T): TableRow | undefined => rows.find(r => r.data === data);
251
+
252
+ /** 展开指定行(按行数据对象匹配);行不存在或不允许展开时不做任何事 */
253
+ export async function expandRow(data: T): Promise<void> {
254
+ const row = findRow(data);
255
+ if (row) await setRowExpanded(row, true);
256
+ }
257
+
258
+ /** 收起指定行;不传参数则收起当前展开的行 */
259
+ export function collapseRow(data?: T): void {
260
+ if (data === undefined) {
261
+ expandSeq++;
262
+ closeExpanded();
263
+ return;
264
+ }
265
+ const row = findRow(data);
266
+ if (row) void setRowExpanded(row, false);
267
+ }
268
+
269
+ /** 切换指定行的展开状态 */
270
+ export async function toggleRowExpand(data: T): Promise<void> {
271
+ const row = findRow(data);
272
+ if (row) await setRowExpanded(row);
273
+ }
274
+
275
+ /** 某行当前是否展开 */
276
+ export function isRowExpanded(data: T): boolean {
277
+ return expandedRow != null && expandedRow.data === data;
278
+ }
279
+
280
+ /** 当前展开行的数据,没有则为 null */
281
+ export function getExpandedRow(): T | null {
282
+ return expandedRow?.data ?? null;
283
+ }
284
+
285
+ // ---- 行级触发(整行单击 / 双击) ----
286
+
287
+ const handleRowClick = (row: TableRow, e: MouseEvent) => {
288
+ if (expandTrigger !== 'click' || isInteractiveTarget(e)) return;
289
+ void setRowExpanded(row);
290
+ };
291
+
292
+ const handleRowDblClick = (row: TableRow, e: MouseEvent) => {
293
+ if (expandTrigger !== 'dblclick' || isInteractiveTarget(e)) return;
294
+ e.preventDefault();
295
+ void setRowExpanded(row);
296
+ };
297
+
298
+ // ---- 行内自定义事件:render cell / ActionCell / 操作列 / 展开区内部都可以派发 ----
299
+
300
+ const handleExpandRequest = (row: TableRow, e: Event) => {
301
+ if (!expandable) return;
302
+ e.stopPropagation();
303
+ const detail = (e as CustomEvent<RowExpandRequest>).detail;
304
+ void setRowExpanded(row, detail?.expanded);
305
+ };
306
+
307
+ // 传给展开区组件的上下文
308
+ let expandContext = $derived.by<RowExpandContext<T> | null>(() => {
309
+ const row = expandedRow;
310
+ if (!row) return null;
311
+ const data = row.data as T;
312
+ const actions = actionsColumn?.getActions(data) ?? [];
313
+ return {
314
+ data,
315
+ collapse: () => { void setRowExpanded(row, false); },
316
+ actions,
317
+ invoke: async (label: string) => {
318
+ const action = actions.find(a => a.label === label);
319
+ if (!action || action.disabled) return false;
320
+ await action.handler(data);
321
+ return true;
322
+ }
323
+ };
324
+ });
325
+
326
+ const rowEvents: RowEventHandlers<TableRow> = {
327
+ onRowClick: handleRowClick,
328
+ onRowDblClick: handleRowDblClick,
329
+ onExpandRequest: handleExpandRequest,
330
+ };
195
331
  </script>
196
332
 
197
333
  <div id="tab-{id}" {style} class="topside-data-table" class:cell-border={colBorder} class:row-border={rowBorder}>
@@ -204,7 +340,10 @@
204
340
  {orderColumn}
205
341
  {onCellClick}
206
342
  {indicatorColumn}
207
- bind:expandRow
343
+ expandRow={expandedRow}
344
+ expandable={showExpandToggle}
345
+ isExpandable={isExpandableRow}
346
+ {rowEvents}
208
347
  {rowHeight}
209
348
  {headerHeight}
210
349
  {fixedCols}
@@ -227,7 +366,9 @@
227
366
  {tabWidth}
228
367
  bind:rows
229
368
  bind:scrollTop
230
- {expandRow}
369
+ expandRow={expandedRow}
370
+ {expandContext}
371
+ {rowEvents}
231
372
  {rowHeight}
232
373
  {headerHeight}
233
374
  {list}
@@ -245,6 +386,6 @@
245
386
  {/if}
246
387
 
247
388
  {#if actionsColumn}
248
- <ActionPanel {expandRow} {rowHeight} {headerHeight} {actionsColumn} {rows} {inlineRowHeight} bind:scrollTop {vwindow} showSummary={tableSummary != null} />
389
+ <ActionPanel expandRow={expandedRow} {rowEvents} {rowHeight} {headerHeight} {actionsColumn} {rows} {inlineRowHeight} bind:scrollTop {vwindow} showSummary={tableSummary != null} />
249
390
  {/if}
250
391
  </div>
@@ -2,6 +2,7 @@ import type DataColumn from "./lib/DataColumn";
2
2
  import type ActionsColumn from "./lib/ActionsColumn";
3
3
  import type IndicatorColumn from "./lib/IndicatorColumn";
4
4
  import type { GetRowFontStyle } from "../types";
5
+ import type RowExpansion from "./lib/RowExpansion";
5
6
  declare function $$render<T = any>(): {
6
7
  props: {
7
8
  columns: Array<DataColumn>;
@@ -32,8 +33,21 @@ declare function $$render<T = any>(): {
32
33
  * 分页场景下用它显示服务端算好的全量合计(数字串会按数字格式化)。
33
34
  */
34
35
  summaryValues?: Record<string, any> | null;
36
+ /**
37
+ * 行展开配置:展开区组件、是否显示指示列图标(默认显示)、整行单击 / 双击触发等。
38
+ * 不配置图标时可以用组件方法 expandRow / collapseRow / toggleRowExpand,
39
+ * 或在行内(自定义单元格、操作列、展开区)派发 ROW_EXPAND_EVENT 事件来控制。
40
+ * 同一时间只展开一行。
41
+ */
42
+ rowExpansion?: RowExpansion<T> | null;
43
+ };
44
+ exports: {
45
+ expandRow: (data: T) => Promise<void>;
46
+ collapseRow: (data?: T) => void;
47
+ toggleRowExpand: (data: T) => Promise<void>;
48
+ isRowExpanded: (data: T) => boolean;
49
+ getExpandedRow: () => T | null;
35
50
  };
36
- exports: {};
37
51
  bindings: "inlineRowHeight" | "selectedRows";
38
52
  slots: {};
39
53
  events: {};
@@ -43,7 +57,13 @@ declare class __sveltets_Render<T = any> {
43
57
  events(): ReturnType<typeof $$render<T>>['events'];
44
58
  slots(): ReturnType<typeof $$render<T>>['slots'];
45
59
  bindings(): "inlineRowHeight" | "selectedRows";
46
- exports(): {};
60
+ exports(): {
61
+ expandRow: (data: T) => Promise<void>;
62
+ collapseRow: (data?: T | undefined) => void;
63
+ toggleRowExpand: (data: T) => Promise<void>;
64
+ isRowExpanded: (data: T) => boolean;
65
+ getExpandedRow: () => T | null;
66
+ };
47
67
  }
48
68
  interface $$IsomorphicComponent {
49
69
  new <T = any>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
@@ -55,6 +55,7 @@ import type { DataColumn, ActionsColumn, IndicatorColumn, RowAction } from "@tic
55
55
  | `style` | `string` | `""` | Additional CSS styles |
56
56
  | `summaryLabel` | `string` | i18n `dataTable.summaryLabel` ("Total") | Label of the summary row; empty string hides it |
57
57
  | `summaryValues` | `Record<string, any> \| null` | `null` | External totals keyed by field; override local aggregation (server-side totals for paged data) |
58
+ | `rowExpansion` | `RowExpansion \| null` | `null` | Row expansion config (inline component, toggle icon, click / double-click trigger…), see [Row Expansion](#row-expansion) |
58
59
 
59
60
  ## Types
60
61
 
@@ -134,7 +135,28 @@ interface IndicatorColumn {
134
135
  width: number; // Column width in pixels
135
136
  selectable?: boolean; // Whether rows are selectable
136
137
  showRowNo?: boolean; // Whether to show row numbers
137
- buildInlineComponent?: (data: any) => Promise<any>; // Inline component builder
138
+ /** @deprecated use the DataTable `rowExpansion` prop instead */
139
+ buildInlineComponent?: (data: any) => Promise<any>;
140
+ }
141
+ ```
142
+
143
+ ### RowExpansion
144
+
145
+ ```typescript
146
+ interface RowExpansion<T = any> {
147
+ build: (data: T) => Promise<Component> | Component; // inline component builder (may be async)
148
+ showToggle?: boolean; // show the expand icon in the indicator column, default true (needs indicatorColumn)
149
+ trigger?: 'click' | 'dblclick'; // toggle by clicking / double-clicking the row; unset by default
150
+ isExpandable?: (data: T) => boolean; // which rows may expand
151
+ onExpandChange?: (data: T | null) => void; // fired on expand / collapse (null when collapsed)
152
+ }
153
+
154
+ // Props received by the inline component: data (row data) and context
155
+ interface RowExpandContext<T = any> {
156
+ data: T;
157
+ collapse: () => void; // collapse the expanded row
158
+ actions: Array<RowAction<T>>; // the row's actions (from actionsColumn.getActions)
159
+ invoke: (label: string) => Promise<boolean>; // run a row action by label
138
160
  }
139
161
  ```
140
162
 
@@ -486,60 +508,36 @@ Using the custom component in column definitions:
486
508
  import OrderDetails from "./OrderDetails.svelte";
487
509
 
488
510
  const columns = [
489
- { text: "Order ID", field: "orderId", width: 120 },
490
- { text: "Customer", field: "customer", width: 150 },
491
- { text: "Total", field: "total", width: 100, align: "right",
511
+ { title: "Order ID", field: "orderId", width: 120 },
512
+ { title: "Customer", field: "customer", width: 150 },
513
+ { title: "Total", field: "total", width: 100, align: "right",
492
514
  formatter: (value) => `$${value?.toFixed(2) ?? '0.00'}`
493
515
  },
494
- { text: "Status", field: "status", width: 120, align: "center" },
495
- { text: "Date", field: "orderDate", width: 120 }
516
+ { title: "Status", field: "status", width: 120, align: "center" }
496
517
  ];
497
518
 
498
- const indicatorColumn = {
499
- width: 50,
500
- selectable: false,
501
- showRowNo: false,
502
- buildInlineComponent: async (orderData) => {
503
- // Simulate API call to fetch order details
504
- await new Promise(resolve => setTimeout(resolve, 1000));
519
+ const indicatorColumn = { width: 50 };
520
+
521
+ const rowExpansion = {
522
+ build: async (orderData) => {
523
+ // e.g. fetch the details first, then pick the component
524
+ await new Promise(resolve => setTimeout(resolve, 300));
505
525
  return OrderDetails;
506
526
  }
507
527
  };
508
-
509
- const orders = [
510
- {
511
- orderId: "ORD-001",
512
- customer: "John Doe",
513
- total: 299.99,
514
- status: "Shipped",
515
- orderDate: "2024-01-15",
516
- items: [
517
- { product: "Laptop", quantity: 1, price: 299.99 }
518
- ]
519
- },
520
- {
521
- orderId: "ORD-002",
522
- customer: "Jane Smith",
523
- total: 149.98,
524
- status: "Processing",
525
- orderDate: "2024-01-14",
526
- items: [
527
- { product: "Mouse", quantity: 2, price: 24.99 },
528
- { product: "Keyboard", quantity: 1, price: 99.99 }
529
- ]
530
- }
531
- ];
532
528
  </script>
533
529
 
534
530
  <DataTable
535
531
  {columns}
536
532
  list={orders}
537
533
  {indicatorColumn}
538
- inlineRowHeight={200}
534
+ {rowExpansion}
539
535
  emptyText="No orders found"
540
536
  />
541
537
  ```
542
538
 
539
+ See [Row Expansion](#row-expansion) for other triggers and for closing / running row actions from inside the expanded panel.
540
+
543
541
  ### Table with Frozen Columns
544
542
 
545
543
  ```svelte
@@ -837,6 +835,89 @@ Set `summary` on any column (or pass `summaryValues` for a column's field) and a
837
835
  - **Paging**: pass server totals via `summaryValues` (keyed by field); they win over local aggregation and work on columns without `summary`.
838
836
  - Background: CSS variable `--topside-data-table-summary-bg` (defaults to the header background).
839
837
 
838
+ ## Row Expansion
839
+
840
+ Configured through `rowExpansion`. Only one row is expanded at a time; the inline component receives `data` (the row data) and `context` (a `RowExpandContext`).
841
+
842
+ ### Triggers
843
+
844
+ ```svelte
845
+ <script>
846
+ import DataTable from "@ticatec/topside-elements/DataTable";
847
+ import OrderDetails from "./OrderDetails.svelte";
848
+
849
+ let table;
850
+
851
+ const rowExpansion = {
852
+ build: async (order) => OrderDetails,
853
+ showToggle: false, // no expand icon in the indicator column (default true)
854
+ trigger: 'dblclick', // double-click the row to toggle; 'click' for single click (clicks on buttons / links / inputs are ignored)
855
+ isExpandable: (order) => order.items?.length > 0,
856
+ onExpandChange: (order) => console.log('expanded:', order) // null when collapsed
857
+ };
858
+
859
+ const actionsColumn = {
860
+ width: 120,
861
+ getActions: (order) => [
862
+ { label: 'Edit', handler: () => edit(order) },
863
+ // an action button can drive expansion through the component methods
864
+ { label: 'Details', handler: () => table.toggleRowExpand(order) }
865
+ ]
866
+ };
867
+ </script>
868
+
869
+ <DataTable bind:this={table} {columns} {list} {rowExpansion} {actionsColumn} />
870
+ <button onclick={() => table.expandRow(list[0])}>Expand first row</button>
871
+ <button onclick={() => table.collapseRow()}>Collapse</button>
872
+ ```
873
+
874
+ Component methods (call them on the `bind:this` instance; rows are matched by data object identity):
875
+
876
+ | Method | Description |
877
+ |--------|-------------|
878
+ | `expandRow(data)` | Expand the row (ignored when already expanded, not allowed by `isExpandable`, or not found); returns a Promise |
879
+ | `collapseRow(data?)` | Collapse the row; without an argument collapses whatever is expanded |
880
+ | `toggleRowExpand(data)` | Toggle the row; returns a Promise |
881
+ | `isRowExpanded(data)` | Whether the row is expanded |
882
+ | `getExpandedRow()` | Data of the expanded row, or `null` |
883
+
884
+ ### Triggering from a custom cell
885
+
886
+ A custom `render` component or an `ActionCell` does not need the table instance: dispatch a request from any element inside the row and the DataTable resolves the row itself.
887
+
888
+ ```svelte
889
+ <!-- OrderNoCell.svelte -->
890
+ <script>
891
+ import { requestRowToggle } from "@ticatec/topside-elements/DataTable";
892
+ let { data } = $props();
893
+ </script>
894
+
895
+ <a href="#" onclick={(e) => { e.preventDefault(); requestRowToggle(e.currentTarget); }}>{data.orderId}</a>
896
+ ```
897
+
898
+ Helpers: `requestRowExpand(el)`, `requestRowCollapse(el)`, `requestRowToggle(el)`. You can also dispatch `ROW_EXPAND_EVENT` (`topside-row-expand`, `detail: { expanded?: boolean }`, `bubbles: true`) yourself. Requests are ignored when no `rowExpansion` is configured.
899
+
900
+ ### Closing / running row actions from the expanded panel
901
+
902
+ ```svelte
903
+ <!-- OrderDetails.svelte -->
904
+ <script>
905
+ let { data, context } = $props();
906
+ </script>
907
+
908
+ <div>
909
+ {#each context.actions as action}
910
+ <button disabled={action.disabled} onclick={() => action.handler(data)}>{action.label}</button>
911
+ {/each}
912
+ <button onclick={() => context.invoke('Edit')}>Edit</button>
913
+ <button onclick={() => context.collapse()}>Close</button>
914
+ </div>
915
+ ```
916
+
917
+ `context.actions` is the result of `actionsColumn.getActions(data)` (an empty array without an `actionsColumn`); deeply nested children can also call `requestRowCollapse(el)` to close their row.
918
+
919
+ > Compatibility: the legacy `indicatorColumn.buildInlineComponent` still works and is equivalent to `rowExpansion = { build }`.
920
+
840
921
  ## Styling
841
922
 
842
923
  The DataTable component supports extensive styling customization through CSS custom properties and classes:
@@ -55,6 +55,7 @@ import type { DataColumn, ActionsColumn, IndicatorColumn, RowAction } from "@tic
55
55
  | `style` | `string` | `""` | 附加 CSS 样式 |
56
56
  | `summaryLabel` | `string` | i18n `dataTable.summaryLabel`(合计) | 合计行标签,传空串不显示 |
57
57
  | `summaryValues` | `Record<string, any> \| null` | `null` | 外部合计值(按 field),优先于本地计算,用于分页时显示服务端全量合计 |
58
+ | `rowExpansion` | `RowExpansion \| null` | `null` | 行展开配置(展开区组件、是否显示图标、单击 / 双击触发等),见[行展开](#行展开) |
58
59
 
59
60
  ## 类型
60
61
 
@@ -134,7 +135,28 @@ interface IndicatorColumn {
134
135
  width: number; // 列宽(像素)
135
136
  selectable?: boolean; // 行是否可选择
136
137
  showRowNo?: boolean; // 是否显示行号
137
- buildInlineComponent?: (data: any) => Promise<any>; // 内联组件生成器
138
+ /** @deprecated 请改用 DataTable 的 rowExpansion 属性 */
139
+ buildInlineComponent?: (data: any) => Promise<any>;
140
+ }
141
+ ```
142
+
143
+ ### RowExpansion
144
+
145
+ ```typescript
146
+ interface RowExpansion<T = any> {
147
+ build: (data: T) => Promise<Component> | Component; // 展开区组件生成器(可异步)
148
+ showToggle?: boolean; // 指示列是否显示展开图标,默认 true(需配置 indicatorColumn)
149
+ trigger?: 'click' | 'dblclick'; // 整行单击 / 双击切换展开,默认不设置
150
+ isExpandable?: (data: T) => boolean; // 哪些行允许展开
151
+ onExpandChange?: (data: T | null) => void; // 展开 / 收起回调
152
+ }
153
+
154
+ // 展开区组件收到的属性:data(行数据)与 context
155
+ interface RowExpandContext<T = any> {
156
+ data: T;
157
+ collapse: () => void; // 关闭当前展开行
158
+ actions: Array<RowAction<T>>; // 本行的行动作(来自 actionsColumn.getActions)
159
+ invoke: (label: string) => Promise<boolean>; // 按 label 执行行动作
138
160
  }
139
161
  ```
140
162
 
@@ -486,60 +508,36 @@ interface RowAction {
486
508
  import OrderDetails from "./OrderDetails.svelte";
487
509
 
488
510
  const columns = [
489
- { text: "订单ID", field: "orderId", width: 120 },
490
- { text: "客户", field: "customer", width: 150 },
491
- { text: "总计", field: "total", width: 100, align: "right",
511
+ { title: "订单ID", field: "orderId", width: 120 },
512
+ { title: "客户", field: "customer", width: 150 },
513
+ { title: "总计", field: "total", width: 100, align: "right",
492
514
  formatter: (value) => `¥${value?.toFixed(2) ?? '0.00'}`
493
515
  },
494
- { text: "状态", field: "status", width: 120, align: "center" },
495
- { text: "日期", field: "orderDate", width: 120 }
516
+ { title: "状态", field: "status", width: 120, align: "center" }
496
517
  ];
497
518
 
498
- const indicatorColumn = {
499
- width: 50,
500
- selectable: false,
501
- showRowNo: false,
502
- buildInlineComponent: async (orderData) => {
503
- // 模拟 API 调用以获取订单详情
504
- await new Promise(resolve => setTimeout(resolve, 1000));
519
+ const indicatorColumn = { width: 50 };
520
+
521
+ const rowExpansion = {
522
+ build: async (orderData) => {
523
+ // 可以先请求明细再决定展开区组件
524
+ await new Promise(resolve => setTimeout(resolve, 300));
505
525
  return OrderDetails;
506
526
  }
507
527
  };
508
-
509
- const orders = [
510
- {
511
- orderId: "ORD-001",
512
- customer: "张伟",
513
- total: 2999.99,
514
- status: "已发货",
515
- orderDate: "2024-01-15",
516
- items: [
517
- { product: "笔记本电脑", quantity: 1, price: 2999.99 }
518
- ]
519
- },
520
- {
521
- orderId: "ORD-002",
522
- customer: "李娜",
523
- total: 1499.98,
524
- status: "处理中",
525
- orderDate: "2024-01-14",
526
- items: [
527
- { product: "鼠标", quantity: 2, price: 249.99 },
528
- { product: "键盘", quantity: 1, price: 999.99 }
529
- ]
530
- }
531
- ];
532
528
  </script>
533
529
 
534
530
  <DataTable
535
531
  {columns}
536
532
  list={orders}
537
533
  {indicatorColumn}
538
- inlineRowHeight={200}
534
+ {rowExpansion}
539
535
  emptyText="未找到订单"
540
536
  />
541
537
  ```
542
538
 
539
+ 更多触发方式与展开区内关闭 / 调用行动作见下文[行展开](#行展开)。
540
+
543
541
  ### 带有冻结列的表格
544
542
 
545
543
  ```svelte
@@ -862,6 +860,89 @@ const columns: DataColumn[] = [
862
860
 
863
861
  合计行背景色可用 CSS 变量 `--topside-data-table-summary-bg` 覆盖(默认同表头)。
864
862
 
863
+ ## 行展开
864
+
865
+ 通过 `rowExpansion` 配置。同一时间只展开一行;展开区组件收到 `data`(行数据)与 `context`(`RowExpandContext`)两个属性。
866
+
867
+ ### 触发方式
868
+
869
+ ```svelte
870
+ <script>
871
+ import DataTable from "@ticatec/topside-elements/DataTable";
872
+ import OrderDetails from "./OrderDetails.svelte";
873
+
874
+ let table;
875
+
876
+ const rowExpansion = {
877
+ build: async (order) => OrderDetails,
878
+ showToggle: false, // 不在指示列显示展开图标(默认 true)
879
+ trigger: 'dblclick', // 双击行切换;'click' 为单击(点到按钮 / 链接 / 输入框不会触发)
880
+ isExpandable: (order) => order.items?.length > 0,
881
+ onExpandChange: (order) => console.log('expanded:', order) // 收起时为 null
882
+ };
883
+
884
+ const actionsColumn = {
885
+ width: 120,
886
+ getActions: (order) => [
887
+ { label: '编辑', handler: () => edit(order) },
888
+ // 操作列按钮通过组件方法控制展开
889
+ { label: '明细', handler: () => table.toggleRowExpand(order) }
890
+ ]
891
+ };
892
+ </script>
893
+
894
+ <DataTable bind:this={table} {columns} {list} {rowExpansion} {actionsColumn} />
895
+ <button onclick={() => table.expandRow(list[0])}>展开第一行</button>
896
+ <button onclick={() => table.collapseRow()}>收起</button>
897
+ ```
898
+
899
+ 组件方法(`bind:this` 后调用,按行数据对象匹配):
900
+
901
+ | 方法 | 说明 |
902
+ |------|------|
903
+ | `expandRow(data)` | 展开指定行(已展开 / 不允许展开 / 找不到时忽略),返回 Promise |
904
+ | `collapseRow(data?)` | 收起指定行;不传参数收起当前展开行 |
905
+ | `toggleRowExpand(data)` | 切换指定行,返回 Promise |
906
+ | `isRowExpanded(data)` | 该行是否展开 |
907
+ | `getExpandedRow()` | 当前展开行的数据,没有为 `null` |
908
+
909
+ ### 在自定义单元格里触发
910
+
911
+ 自定义 `render` 组件或 `ActionCell` 不需要拿到表格实例:在行内任意元素上派发事件即可,DataTable 会定位到所在行。
912
+
913
+ ```svelte
914
+ <!-- OrderNoCell.svelte -->
915
+ <script>
916
+ import { requestRowToggle } from "@ticatec/topside-elements/DataTable";
917
+ let { data } = $props();
918
+ </script>
919
+
920
+ <a href="#" onclick={(e) => { e.preventDefault(); requestRowToggle(e.currentTarget); }}>{data.orderId}</a>
921
+ ```
922
+
923
+ 可用的助手:`requestRowExpand(el)`、`requestRowCollapse(el)`、`requestRowToggle(el)`;也可以自己派发 `ROW_EXPAND_EVENT`(`topside-row-expand`,`detail: { expanded?: boolean }`,需要 `bubbles: true`)。没有配置 `rowExpansion` 时这些请求被忽略。
924
+
925
+ ### 展开区内关闭 / 调用行动作
926
+
927
+ ```svelte
928
+ <!-- OrderDetails.svelte -->
929
+ <script>
930
+ let { data, context } = $props();
931
+ </script>
932
+
933
+ <div>
934
+ {#each context.actions as action}
935
+ <button disabled={action.disabled} onclick={() => action.handler(data)}>{action.label}</button>
936
+ {/each}
937
+ <button onclick={() => context.invoke('编辑')}>编辑</button>
938
+ <button onclick={() => context.collapse()}>关闭</button>
939
+ </div>
940
+ ```
941
+
942
+ `context.actions` 即 `actionsColumn.getActions(data)` 的结果(没有 `actionsColumn` 时为空数组);嵌套很深的子组件也可以用 `requestRowCollapse(el)` 关闭所在行。
943
+
944
+ > 兼容:旧写法 `indicatorColumn.buildInlineComponent` 仍然有效,等价于 `rowExpansion = { build }`。
945
+
865
946
  ## 样式
866
947
 
867
948
  DataTable 组件通过 CSS 自定义属性和类支持广泛的样式定制: