@beppla/tapas-ui 1.4.8 → 1.4.10

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.
@@ -265,6 +265,81 @@ For tables with hundreds or thousands of rows, enable virtual scrolling for opti
265
265
  />
266
266
  ```
267
267
 
268
+ ## Sortable Columns
269
+
270
+ Add sorting functionality to column headers with built-in or custom rendering:
271
+
272
+ ### Built-in Sorting UI
273
+
274
+ ```tsx
275
+ import { StatisticsTable } from '@beppla/tapas-ui';
276
+
277
+ function SortableTable() {
278
+ const [sortConfig, setSortConfig] = useState<{
279
+ column: string | null;
280
+ direction: 'asc' | 'desc' | null;
281
+ }>({ column: null, direction: null });
282
+
283
+ const handleSort = (columnKey: string) => {
284
+ setSortConfig(prev => ({
285
+ column: columnKey,
286
+ direction: prev.column === columnKey && prev.direction === 'asc'
287
+ ? 'desc'
288
+ : 'asc'
289
+ }));
290
+ };
291
+
292
+ const columns = [
293
+ {
294
+ key: 'store1',
295
+ title: 'Store A (€)',
296
+ width: 180,
297
+ sortable: true,
298
+ sortDirection: sortConfig.column === 'store1' ? sortConfig.direction : null,
299
+ onSort: handleSort,
300
+ },
301
+ {
302
+ key: 'store2',
303
+ title: 'Store B (€)',
304
+ width: 180,
305
+ sortable: true,
306
+ sortDirection: sortConfig.column === 'store2' ? sortConfig.direction : null,
307
+ onSort: handleSort,
308
+ },
309
+ ];
310
+
311
+ return <StatisticsTable rows={rows} columns={columns} cells={cells} />;
312
+ }
313
+ ```
314
+
315
+ ### Custom Header Rendering
316
+
317
+ For complete control over header appearance:
318
+
319
+ ```tsx
320
+ const columns = [
321
+ {
322
+ key: 'store1',
323
+ title: 'Store A',
324
+ width: 180,
325
+ renderHeader: (column) => (
326
+ <TouchableOpacity
327
+ onPress={() => handleSort(column.key)}
328
+ style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}
329
+ >
330
+ <Text style={{ fontSize: 14, fontWeight: '600' }}>
331
+ {column.title}
332
+ </Text>
333
+ <View>
334
+ <Icon name="arrow-up" size={10} color={sortDirection === 'asc' ? '#000' : '#ccc'} />
335
+ <Icon name="arrow-down" size={10} color={sortDirection === 'desc' ? '#000' : '#ccc'} />
336
+ </View>
337
+ </TouchableOpacity>
338
+ ),
339
+ },
340
+ ];
341
+ ```
342
+
268
343
  ## Internationalization (i18n)
269
344
 
270
345
  Customize loading and empty state texts for different languages:
@@ -0,0 +1,271 @@
1
+ # StatisticsTable 排序功能
2
+
3
+ ## 概述
4
+
5
+ StatisticsTable 组件支持两种方式实现列排序:
6
+ 1. **内置排序 UI**:使用 `sortable`, `sortDirection`, `onSort` 属性
7
+ 2. **完全自定义**:使用 `renderHeader` 属性完全控制表头渲染
8
+
9
+ ## 方式一:内置排序 UI
10
+
11
+ ### 功能特性
12
+
13
+ - ✅ 内置排序指示器(▲▼箭头)
14
+ - ✅ 自动高亮当前排序方向
15
+ - ✅ 点击表头切换排序
16
+ - ✅ 主题颜色自适应
17
+
18
+ ### 使用示例
19
+
20
+ ```tsx
21
+ import React, { useState } from 'react';
22
+ import { StatisticsTable } from '@beppla/tapas-ui';
23
+
24
+ function SortableTable() {
25
+ const [sortConfig, setSortConfig] = useState({
26
+ column: null,
27
+ direction: null,
28
+ });
29
+
30
+ const handleSort = (columnKey: string) => {
31
+ setSortConfig(prev => ({
32
+ column: columnKey,
33
+ direction: prev.column === columnKey && prev.direction === 'asc'
34
+ ? 'desc'
35
+ : 'asc'
36
+ }));
37
+ };
38
+
39
+ const columns = [
40
+ {
41
+ key: 'store1',
42
+ title: 'Store A (€)',
43
+ width: 180,
44
+ sortable: true, // 启用排序
45
+ sortDirection: sortConfig.column === 'store1' ? sortConfig.direction : null,
46
+ onSort: handleSort,
47
+ },
48
+ {
49
+ key: 'store2',
50
+ title: 'Store B (€)',
51
+ width: 180,
52
+ sortable: true,
53
+ sortDirection: sortConfig.column === 'store2' ? sortConfig.direction : null,
54
+ onSort: handleSort,
55
+ },
56
+ ];
57
+
58
+ // 对数据进行排序
59
+ let sortedRows = [...rows];
60
+ if (sortConfig.column && sortConfig.direction) {
61
+ sortedRows = sortedRows.sort((a, b) => {
62
+ const cellA = cells.find(c => c.rowKey === a.key && c.columnKey === sortConfig.column);
63
+ const cellB = cells.find(c => c.rowKey === b.key && c.columnKey === sortConfig.column);
64
+ const valueA = cellA?.amount || 0;
65
+ const valueB = cellB?.amount || 0;
66
+ return sortConfig.direction === 'asc' ? valueA - valueB : valueB - valueA;
67
+ });
68
+ }
69
+
70
+ return (
71
+ <StatisticsTable
72
+ rows={sortedRows}
73
+ columns={columns}
74
+ cells={cells}
75
+ />
76
+ );
77
+ }
78
+ ```
79
+
80
+ ### 内置 UI 说明
81
+
82
+ **排序指示器:**
83
+ - 两个箭头:▲ 升序,▼ 降序
84
+ - 未激活:浅灰色 `rgba(0, 0, 0, 0.26)`
85
+ - 激活时:深色 `rgba(0, 0, 0, 0.87)`
86
+
87
+ **点击行为:**
88
+ - 第一次点击:升序(▲ 高亮)
89
+ - 第二次点击:降序(▼ 高亮)
90
+ - 第三次点击:取消排序(视具体实现)
91
+
92
+ ## 方式二:完全自定义 Header
93
+
94
+ ### 使用 renderHeader
95
+
96
+ ```tsx
97
+ const columns = [
98
+ {
99
+ key: 'store1',
100
+ title: 'Store A (€)',
101
+ width: 200,
102
+ renderHeader: (column) => (
103
+ <TouchableOpacity
104
+ onPress={() => handleCustomSort(column.key)}
105
+ style={{
106
+ flexDirection: 'row',
107
+ alignItems: 'center',
108
+ gap: 8,
109
+ padding: 4,
110
+ }}
111
+ >
112
+ <Text style={{
113
+ fontSize: 14,
114
+ fontWeight: '600',
115
+ color: '#333',
116
+ }}>
117
+ {column.title}
118
+ </Text>
119
+
120
+ {/* 自定义排序图标 */}
121
+ <View style={{ flexDirection: 'column', gap: 2 }}>
122
+ <TapasIcon
123
+ name="caret-up"
124
+ size={10}
125
+ color={currentSort.column === column.key && currentSort.dir === 'asc'
126
+ ? '#895F38' // 品牌色
127
+ : '#CCC' // 灰色
128
+ }
129
+ />
130
+ <TapasIcon
131
+ name="caret-down"
132
+ size={10}
133
+ color={currentSort.column === column.key && currentSort.dir === 'desc'
134
+ ? '#895F38'
135
+ : '#CCC'
136
+ }
137
+ />
138
+ </View>
139
+ </TouchableOpacity>
140
+ ),
141
+ },
142
+ ];
143
+ ```
144
+
145
+ ### 自定义样式示例
146
+
147
+ ```tsx
148
+ renderHeader: (column) => (
149
+ <View style={{
150
+ flexDirection: 'row',
151
+ alignItems: 'center',
152
+ justifyContent: 'space-between',
153
+ width: '100%',
154
+ }}>
155
+ <Text style={{ fontSize: 14, fontWeight: '700' }}>
156
+ {column.title}
157
+ </Text>
158
+
159
+ {/* 自定义徽章 */}
160
+ {column.key === 'total' && (
161
+ <View style={{
162
+ backgroundColor: '#F55523',
163
+ borderRadius: 12,
164
+ paddingHorizontal: 6,
165
+ paddingVertical: 2,
166
+ }}>
167
+ <Text style={{ fontSize: 10, color: '#FFF' }}>Total</Text>
168
+ </View>
169
+ )}
170
+
171
+ {/* 排序按钮 */}
172
+ <SortButton active={isSorted} direction={direction} />
173
+ </View>
174
+ )
175
+ ```
176
+
177
+ ## 新增属性
178
+
179
+ ### StatisticsTableColumn
180
+
181
+ | 属性 | 类型 | 说明 |
182
+ |------|------|------|
183
+ | `sortable` | `boolean` | 是否可排序(启用内置排序 UI) |
184
+ | `sortDirection` | `'asc' \| 'desc' \| null` | 当前排序方向 |
185
+ | `onSort` | `(columnKey: string) => void` | 排序回调函数 |
186
+ | `renderHeader` | `(column) => ReactNode` | 完全自定义表头渲染 |
187
+
188
+ ## 实现细节
189
+
190
+ ### 内置排序渲染逻辑
191
+
192
+ ```typescript
193
+ // 默认渲染(带排序支持)
194
+ const isSortable = column.sortable && column.onSort;
195
+
196
+ const HeaderContent = (
197
+ <View style={styles.headerContent}>
198
+ <Text style={styles.headerText}>
199
+ {column.title}
200
+ </Text>
201
+ {isSortable && (
202
+ <View style={styles.sortIndicator}>
203
+ <Text style={[
204
+ styles.sortArrow,
205
+ column.sortDirection === 'asc' && styles.sortArrowActive
206
+ ]}>▲</Text>
207
+ <Text style={[
208
+ styles.sortArrow,
209
+ column.sortDirection === 'desc' && styles.sortArrowActive
210
+ ]}>▼</Text>
211
+ </View>
212
+ )}
213
+ </View>
214
+ );
215
+
216
+ return (
217
+ <TouchableOpacity
218
+ onPress={isSortable ? () => column.onSort!(column.key) : undefined}
219
+ activeOpacity={isSortable ? 0.7 : 1}
220
+ disabled={!isSortable}
221
+ >
222
+ {HeaderContent}
223
+ </TouchableOpacity>
224
+ );
225
+ ```
226
+
227
+ ### 优先级
228
+
229
+ 1. 如果提供了 `renderHeader`:使用完全自定义渲染
230
+ 2. 如果设置了 `sortable` 和 `onSort`:使用内置排序 UI
231
+ 3. 否则:显示普通文本标题
232
+
233
+ ## 使用建议
234
+
235
+ ### 何时使用内置排序 UI?
236
+
237
+ ✅ **推荐使用:**
238
+ - 简单的表格排序需求
239
+ - 想要快速实现排序功能
240
+ - 满意内置的箭头样式
241
+ - 不需要特殊的排序图标
242
+
243
+ ### 何时使用自定义 renderHeader?
244
+
245
+ ✅ **推荐使用:**
246
+ - 需要特定的排序图标或样式
247
+ - 表头需要额外的元素(徽章、图标、提示等)
248
+ - 需要完全控制表头布局
249
+ - 需要与设计系统保持一致
250
+
251
+ ## 注意事项
252
+
253
+ 1. **数据排序逻辑**:组件本身不处理数据排序,需要用户自己实现排序逻辑
254
+ 2. **状态管理**:排序状态需要用户管理(使用 useState)
255
+ 3. **性能**:对于大数据集,建议配合虚拟滚动使用
256
+ 4. **占位列**:占位列会自动跳过排序功能
257
+
258
+ ## Storybook 示例
259
+
260
+ - `SortableColumns` - 内置排序 UI 演示
261
+ - `CustomHeaderRendering` - 完全自定义表头演示
262
+
263
+ ## 相关更新
264
+
265
+ - ✅ StatisticsTableColumn 接口添加排序相关属性
266
+ - ✅ 表头支持点击事件
267
+ - ✅ 内置排序指示器样式
268
+ - ✅ 完整的自定义渲染支持
269
+ - ✅ 2 个 Storybook 示例
270
+ - ✅ 2 个测试用例
271
+
@@ -227,11 +227,11 @@ function StatisticsTable({
227
227
  },
228
228
  // Hover 行
229
229
  rowHovered: {
230
- backgroundColor: colors.colorSurface1 // 白色
230
+ backgroundColor: colors.colorSurface8 // rgba(0, 0, 0, 0.04) - 更柔和
231
231
  },
232
- // Hover 单元格 - rgba(0, 0, 0, 0.08)
232
+ // Hover 单元格
233
233
  cellHovered: {
234
- backgroundColor: 'rgba(0, 0, 0, 0.08)'
234
+ backgroundColor: colors.colorSurface8 // rgba(0, 0, 0, 0.04) - 更柔和
235
235
  },
236
236
  // Column Stats 行(Sum/Mean)- 纯白色背景
237
237
  columnStatsRow: {
@@ -673,6 +673,20 @@ function StatisticsTable({
673
673
  onScroll: handleScroll,
674
674
  scrollEventThrottle: 16,
675
675
  style: hideScrollbarStyle,
676
+ onContentSizeChange: () => {
677
+ // 当内容大小变化时,检查是否需要显示滚动阴影
678
+ if (scrollViewRef.current && enableScrollShadow) {
679
+ // React Native 会在 contentSize 变化后自动触发 onScroll
680
+ // 但为了确保状态正确,我们手动触发一次
681
+ setTimeout(() => {
682
+ scrollViewRef.current?.scrollTo?.({
683
+ x: 0,
684
+ y: 0,
685
+ animated: false
686
+ });
687
+ }, 0);
688
+ }
689
+ },
676
690
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
677
691
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
678
692
  style: [styles.header, themedStyles.header],
@@ -685,14 +699,44 @@ function StatisticsTable({
685
699
  })
686
700
  }), displayColumns.map(column => {
687
701
  const colWidth = getColWidth(column);
688
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
702
+ const isPlaceholder = column.key.startsWith('_placeholder_col_');
703
+
704
+ // 如果提供了自定义 header 渲染函数
705
+ if (column.renderHeader && !isPlaceholder) {
706
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
707
+ style: [styles.headerCell, themedStyles.headerCell, {
708
+ width: colWidth
709
+ }],
710
+ children: column.renderHeader(column)
711
+ }, column.key);
712
+ }
713
+
714
+ // 默认渲染(带排序支持)
715
+ const isSortable = column.sortable && column.onSort;
716
+ const HeaderContent = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
717
+ style: styles.headerContent,
718
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
719
+ style: [styles.headerText, themedStyles.headerText],
720
+ children: column.title
721
+ }), isSortable && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
722
+ style: styles.sortIndicator,
723
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
724
+ style: [styles.sortArrow, column.sortDirection === 'asc' && styles.sortArrowActive],
725
+ children: "\u25B2"
726
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
727
+ style: [styles.sortArrow, column.sortDirection === 'desc' && styles.sortArrowActive],
728
+ children: "\u25BC"
729
+ })]
730
+ })]
731
+ });
732
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
689
733
  style: [styles.headerCell, themedStyles.headerCell, {
690
734
  width: colWidth
691
735
  }],
692
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
693
- style: [styles.headerText, themedStyles.headerText],
694
- children: column.title
695
- })
736
+ onPress: isSortable ? () => column.onSort(column.key) : undefined,
737
+ activeOpacity: isSortable ? 0.7 : 1,
738
+ disabled: !isSortable,
739
+ children: HeaderContent
696
740
  }, column.key);
697
741
  })]
698
742
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
@@ -1019,6 +1063,24 @@ const styles = _reactNative.StyleSheet.create({
1019
1063
  fontWeight: '500',
1020
1064
  textAlign: 'center'
1021
1065
  },
1066
+ headerContent: {
1067
+ flexDirection: 'row',
1068
+ alignItems: 'center',
1069
+ justifyContent: 'center',
1070
+ gap: 6
1071
+ },
1072
+ sortIndicator: {
1073
+ flexDirection: 'column',
1074
+ gap: 2
1075
+ },
1076
+ sortArrow: {
1077
+ fontSize: 8,
1078
+ color: 'rgba(0, 0, 0, 0.26)',
1079
+ lineHeight: 8
1080
+ },
1081
+ sortArrowActive: {
1082
+ color: 'rgba(0, 0, 0, 0.87)'
1083
+ },
1022
1084
  row: {
1023
1085
  flexDirection: 'row',
1024
1086
  borderBottomWidth: 1