@beppla/tapas-ui 1.2.36 → 1.4.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.
- package/components/StatisticsTable/StatisticsTable.tsx +721 -430
- package/components/Theme/defaultTheme.ts +2 -0
- package/package/commonjs/StatisticsTable/StatisticsTable.js +714 -437
- package/package/commonjs/StatisticsTable/StatisticsTable.js.map +1 -1
- package/package/commonjs/Theme/defaultTheme.js +6 -2
- package/package/commonjs/Theme/defaultTheme.js.map +1 -1
- package/package/module/StatisticsTable/StatisticsTable.js +708 -431
- package/package/module/StatisticsTable/StatisticsTable.js.map +1 -1
- package/package/module/Theme/defaultTheme.js +6 -2
- package/package/module/Theme/defaultTheme.js.map +1 -1
- package/package/package.json +1 -1
- package/package/typescript/StatisticsTable/StatisticsTable.d.ts +53 -3
- package/package/typescript/StatisticsTable/StatisticsTable.d.ts.map +1 -1
- package/package/typescript/Theme/defaultTheme.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useMemo, useCallback, useState, useRef } from 'react';
|
|
2
|
-
import { View, ScrollView, StyleSheet, Platform, TouchableOpacity, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
3
|
-
import
|
|
1
|
+
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { View, ScrollView, StyleSheet, Platform, TouchableOpacity, NativeScrollEvent, NativeSyntheticEvent, Text, Animated } from 'react-native';
|
|
3
|
+
import { useTheme } from '@rneui/themed';
|
|
4
4
|
import Pagination from '../Pagination/Pagination';
|
|
5
|
+
import Hoverable from '../Hoverable/Hoverable';
|
|
5
6
|
|
|
6
7
|
export type CellAlignment = 'left' | 'center' | 'right';
|
|
7
8
|
export type CellDataType = 'text' | 'number' | 'currency' | 'custom';
|
|
@@ -9,12 +10,12 @@ export type CellDataType = 'text' | 'number' | 'currency' | 'custom';
|
|
|
9
10
|
export interface StatisticsTableColumn {
|
|
10
11
|
key: string;
|
|
11
12
|
title: string;
|
|
12
|
-
width?: number;
|
|
13
|
-
minWidth?: number;
|
|
14
|
-
maxWidth?: number;
|
|
13
|
+
width?: number;
|
|
14
|
+
minWidth?: number;
|
|
15
|
+
maxWidth?: number;
|
|
15
16
|
align?: CellAlignment;
|
|
16
17
|
dataType?: CellDataType;
|
|
17
|
-
isAction?: boolean;
|
|
18
|
+
isAction?: boolean;
|
|
18
19
|
render?: (cell: StatisticsTableCell, rowData: any) => React.ReactNode;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -23,7 +24,7 @@ export interface StatisticsTableCell {
|
|
|
23
24
|
columnKey: string;
|
|
24
25
|
quantity: number;
|
|
25
26
|
amount: number;
|
|
26
|
-
data?: any;
|
|
27
|
+
data?: any;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface StatisticsTableAction {
|
|
@@ -32,18 +33,20 @@ export interface StatisticsTableAction {
|
|
|
32
33
|
icon?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// Tooltip 内容类型
|
|
37
|
+
export interface TooltipContent {
|
|
38
|
+
title?: string;
|
|
39
|
+
subtitle?: string;
|
|
40
|
+
items?: Array<{ label: string; value: string }>;
|
|
41
|
+
customContent?: React.ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
export interface StatisticsTableProps {
|
|
36
|
-
// 行标题(如日期)
|
|
37
45
|
rows: Array<{ key: string; label: string; data?: any; height?: number }>;
|
|
38
|
-
// 列标题(如门店)
|
|
39
46
|
columns: StatisticsTableColumn[];
|
|
40
|
-
// 单元格数据
|
|
41
47
|
cells: StatisticsTableCell[];
|
|
42
|
-
// 是否显示行统计(Sum/Mean 列)
|
|
43
48
|
showRowStats?: boolean;
|
|
44
|
-
// 是否显示列统计(Sum/Mean 行)
|
|
45
49
|
showColumnStats?: boolean;
|
|
46
|
-
// 分页配置
|
|
47
50
|
pagination?: {
|
|
48
51
|
current: number;
|
|
49
52
|
pageSize: number;
|
|
@@ -55,27 +58,125 @@ export interface StatisticsTableProps {
|
|
|
55
58
|
maxHeight?: number;
|
|
56
59
|
rowLabelWidth?: number;
|
|
57
60
|
style?: any;
|
|
58
|
-
// 格式化函数
|
|
59
61
|
formatQuantity?: (quantity: number) => string;
|
|
60
62
|
formatAmount?: (amount: number) => string;
|
|
61
|
-
// 虚拟滚动(性能优化)
|
|
62
63
|
enableVirtualization?: boolean;
|
|
63
64
|
virtualRowHeight?: number;
|
|
64
|
-
// 行点击事件
|
|
65
65
|
onRowPress?: (rowKey: string, data?: any) => void;
|
|
66
|
-
// Action 列配置
|
|
67
66
|
rowActions?: StatisticsTableAction[];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
rowHeight?: number;
|
|
68
|
+
getRowHeight?: (rowKey: string, rowData?: any) => number;
|
|
69
|
+
getColumnWidth?: (columnKey: string, column: StatisticsTableColumn) => number;
|
|
70
|
+
enableScrollShadow?: boolean;
|
|
71
|
+
showScrollIndicator?: boolean;
|
|
72
|
+
statsColumnWidth?: number;
|
|
73
|
+
onCellHover?: (rowKey: string, columnKey: string, isHovered: boolean) => void;
|
|
74
|
+
renderCellTooltip?: (rowKey: string, columnKey: string, cell: StatisticsTableCell, column: StatisticsTableColumn) => TooltipContent | null;
|
|
75
|
+
renderRowStatsTooltip?: (rowKey: string, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }, storeCount: number) => TooltipContent | null;
|
|
76
|
+
renderColumnStatsTooltip?: (columnKey: string, column: StatisticsTableColumn, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }) => TooltipContent | null;
|
|
77
|
+
// 统计列/行表头配置
|
|
78
|
+
rowStatsHeaders?: {
|
|
79
|
+
sum: string;
|
|
80
|
+
mean: string;
|
|
81
|
+
};
|
|
82
|
+
columnStatsLabels?: {
|
|
83
|
+
sum: string;
|
|
84
|
+
mean: string;
|
|
85
|
+
};
|
|
86
|
+
// 表头样式定制
|
|
87
|
+
headerStyle?: {
|
|
88
|
+
backgroundColor?: string;
|
|
89
|
+
textColor?: string;
|
|
90
|
+
fontSize?: number;
|
|
91
|
+
fontWeight?: 'normal' | 'bold' | '500' | '600' | '700';
|
|
92
|
+
};
|
|
93
|
+
// Row Stats 表头样式定制
|
|
94
|
+
rowStatsHeaderStyle?: {
|
|
95
|
+
backgroundColor?: string;
|
|
96
|
+
textColor?: string;
|
|
97
|
+
fontSize?: number;
|
|
98
|
+
fontWeight?: 'normal' | 'bold' | '500' | '600' | '700';
|
|
99
|
+
};
|
|
100
|
+
// Column Stats 标签样式定制
|
|
101
|
+
columnStatsLabelStyle?: {
|
|
102
|
+
textColor?: string;
|
|
103
|
+
fontSize?: number;
|
|
104
|
+
fontWeight?: 'normal' | 'bold' | '500' | '600' | '700';
|
|
105
|
+
};
|
|
106
|
+
// 动画配置
|
|
107
|
+
enableStatsAnimation?: boolean;
|
|
108
|
+
statsAnimationDuration?: number; // 动画持续时间(毫秒)
|
|
77
109
|
}
|
|
78
110
|
|
|
111
|
+
// Tooltip 组件
|
|
112
|
+
const CellTooltip = ({ visible, content, style }: {
|
|
113
|
+
visible: boolean;
|
|
114
|
+
content: TooltipContent | null;
|
|
115
|
+
style?: any;
|
|
116
|
+
}) => {
|
|
117
|
+
if (!visible || !content) return null;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<View style={[tooltipStyles.container, style]}>
|
|
121
|
+
{content.customContent ? (
|
|
122
|
+
content.customContent
|
|
123
|
+
) : (
|
|
124
|
+
<>
|
|
125
|
+
{content.title && <Text style={tooltipStyles.title}>{content.title}</Text>}
|
|
126
|
+
{content.subtitle && <Text style={tooltipStyles.subtitle}>{content.subtitle}</Text>}
|
|
127
|
+
{content.items?.map((item, index) => (
|
|
128
|
+
<View key={index} style={tooltipStyles.row}>
|
|
129
|
+
<Text style={tooltipStyles.label}>{item.label}</Text>
|
|
130
|
+
<Text style={tooltipStyles.value}>{item.value}</Text>
|
|
131
|
+
</View>
|
|
132
|
+
))}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const tooltipStyles = StyleSheet.create({
|
|
140
|
+
container: {
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
backgroundColor: '#FFF',
|
|
143
|
+
borderRadius: 8,
|
|
144
|
+
padding: 12,
|
|
145
|
+
shadowColor: '#000',
|
|
146
|
+
shadowOffset: { width: 0, height: 2 },
|
|
147
|
+
shadowOpacity: 0.15,
|
|
148
|
+
shadowRadius: 8,
|
|
149
|
+
elevation: 5,
|
|
150
|
+
zIndex: 100,
|
|
151
|
+
minWidth: 150,
|
|
152
|
+
},
|
|
153
|
+
title: {
|
|
154
|
+
fontSize: 14,
|
|
155
|
+
fontWeight: '600',
|
|
156
|
+
color: 'rgba(0, 0, 0, 0.87)',
|
|
157
|
+
marginBottom: 4,
|
|
158
|
+
},
|
|
159
|
+
subtitle: {
|
|
160
|
+
fontSize: 12,
|
|
161
|
+
color: 'rgba(0, 0, 0, 0.54)',
|
|
162
|
+
marginBottom: 8,
|
|
163
|
+
},
|
|
164
|
+
row: {
|
|
165
|
+
flexDirection: 'row',
|
|
166
|
+
justifyContent: 'space-between',
|
|
167
|
+
marginTop: 4,
|
|
168
|
+
},
|
|
169
|
+
label: {
|
|
170
|
+
fontSize: 12,
|
|
171
|
+
color: 'rgba(0, 0, 0, 0.54)',
|
|
172
|
+
},
|
|
173
|
+
value: {
|
|
174
|
+
fontSize: 12,
|
|
175
|
+
fontWeight: '600',
|
|
176
|
+
color: 'rgba(0, 0, 0, 0.87)',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
79
180
|
export function StatisticsTable({
|
|
80
181
|
rows,
|
|
81
182
|
columns,
|
|
@@ -98,22 +199,156 @@ export function StatisticsTable({
|
|
|
98
199
|
getRowHeight,
|
|
99
200
|
getColumnWidth,
|
|
100
201
|
enableScrollShadow = true,
|
|
101
|
-
|
|
102
|
-
|
|
202
|
+
showScrollIndicator = false,
|
|
203
|
+
statsColumnWidth = 100,
|
|
204
|
+
onCellHover,
|
|
205
|
+
renderCellTooltip,
|
|
206
|
+
renderRowStatsTooltip,
|
|
207
|
+
renderColumnStatsTooltip,
|
|
208
|
+
rowStatsHeaders = { sum: 'Sum (€)', mean: 'Mean (€)' },
|
|
209
|
+
columnStatsLabels = { sum: 'Sum', mean: 'Mean' },
|
|
210
|
+
headerStyle,
|
|
211
|
+
rowStatsHeaderStyle,
|
|
212
|
+
columnStatsLabelStyle,
|
|
213
|
+
enableStatsAnimation = true,
|
|
214
|
+
statsAnimationDuration = 300,
|
|
103
215
|
}: StatisticsTableProps) {
|
|
216
|
+
const { theme } = useTheme();
|
|
217
|
+
const colors = theme.colors as any;
|
|
218
|
+
|
|
219
|
+
// 动画值
|
|
220
|
+
const rowStatsAnimation = useRef(new Animated.Value(0)).current;
|
|
221
|
+
const columnStatsAnimation = useRef(new Animated.Value(0)).current;
|
|
104
222
|
|
|
105
223
|
// 滚动状态
|
|
106
|
-
const [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
const [verticalScrollState, setVerticalScrollState] = useState({
|
|
224
|
+
const [scrollState, setScrollState] = useState({
|
|
225
|
+
left: 0,
|
|
226
|
+
top: 0,
|
|
111
227
|
isAtStart: true,
|
|
112
228
|
isAtEnd: false,
|
|
229
|
+
isAtTop: true,
|
|
230
|
+
isAtBottom: false,
|
|
113
231
|
});
|
|
114
232
|
|
|
115
|
-
|
|
116
|
-
const
|
|
233
|
+
// Hover 状态
|
|
234
|
+
const [hoveredCell, setHoveredCell] = useState<{
|
|
235
|
+
rowKey: string;
|
|
236
|
+
columnKey: string;
|
|
237
|
+
} | null>(null);
|
|
238
|
+
|
|
239
|
+
const scrollViewRef = useRef<ScrollView>(null);
|
|
240
|
+
const bodyScrollViewRef = useRef<ScrollView>(null);
|
|
241
|
+
const rowStatsScrollRef = useRef<ScrollView>(null);
|
|
242
|
+
|
|
243
|
+
// Column stats 和 Row stats 是互斥的,columnStats 优先
|
|
244
|
+
const actualShowColumnStats = showColumnStats;
|
|
245
|
+
const actualShowRowStats = showRowStats && !showColumnStats;
|
|
246
|
+
|
|
247
|
+
// Row Stats 动画效果
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (enableStatsAnimation && actualShowRowStats) {
|
|
250
|
+
Animated.timing(rowStatsAnimation, {
|
|
251
|
+
toValue: 1,
|
|
252
|
+
duration: statsAnimationDuration,
|
|
253
|
+
useNativeDriver: true,
|
|
254
|
+
}).start();
|
|
255
|
+
} else {
|
|
256
|
+
rowStatsAnimation.setValue(actualShowRowStats ? 1 : 0);
|
|
257
|
+
}
|
|
258
|
+
}, [actualShowRowStats, enableStatsAnimation, statsAnimationDuration, rowStatsAnimation]);
|
|
259
|
+
|
|
260
|
+
// Column Stats 动画效果
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (enableStatsAnimation && actualShowColumnStats) {
|
|
263
|
+
Animated.timing(columnStatsAnimation, {
|
|
264
|
+
toValue: 1,
|
|
265
|
+
duration: statsAnimationDuration,
|
|
266
|
+
useNativeDriver: true,
|
|
267
|
+
}).start();
|
|
268
|
+
} else {
|
|
269
|
+
columnStatsAnimation.setValue(actualShowColumnStats ? 1 : 0);
|
|
270
|
+
}
|
|
271
|
+
}, [actualShowColumnStats, enableStatsAnimation, statsAnimationDuration, columnStatsAnimation]);
|
|
272
|
+
|
|
273
|
+
// 动态样式 - 根据设计图
|
|
274
|
+
const themedStyles = useMemo(() => ({
|
|
275
|
+
container: {
|
|
276
|
+
backgroundColor: colors.colorSurface, // 米色背景
|
|
277
|
+
},
|
|
278
|
+
header: {
|
|
279
|
+
backgroundColor: headerStyle?.backgroundColor || colors.colorSurface, // 米色
|
|
280
|
+
borderBottomColor: colors.colorTableBorder,
|
|
281
|
+
},
|
|
282
|
+
headerCell: {
|
|
283
|
+
borderRightColor: colors.colorTableBorder,
|
|
284
|
+
},
|
|
285
|
+
headerText: {
|
|
286
|
+
color: headerStyle?.textColor || colors.colorTextPrimary,
|
|
287
|
+
fontSize: headerStyle?.fontSize || 14,
|
|
288
|
+
fontWeight: headerStyle?.fontWeight || '500',
|
|
289
|
+
},
|
|
290
|
+
// Row Stats 表头样式
|
|
291
|
+
statsHeaderCell: {
|
|
292
|
+
backgroundColor: rowStatsHeaderStyle?.backgroundColor || 'rgba(0, 0, 0, 0.08)',
|
|
293
|
+
borderRightColor: colors.colorTableBorder,
|
|
294
|
+
borderBottomColor: colors.colorTableBorder,
|
|
295
|
+
},
|
|
296
|
+
statsHeaderText: {
|
|
297
|
+
color: rowStatsHeaderStyle?.textColor || colors.colorTextPrimary,
|
|
298
|
+
fontSize: rowStatsHeaderStyle?.fontSize || 14,
|
|
299
|
+
fontWeight: rowStatsHeaderStyle?.fontWeight || '500',
|
|
300
|
+
},
|
|
301
|
+
// 数据行
|
|
302
|
+
row: {
|
|
303
|
+
borderBottomColor: colors.colorTableBorder,
|
|
304
|
+
backgroundColor: colors.colorSurface, // 米色
|
|
305
|
+
},
|
|
306
|
+
// Hover 行
|
|
307
|
+
rowHovered: {
|
|
308
|
+
backgroundColor: colors.colorSurface1, // 白色
|
|
309
|
+
},
|
|
310
|
+
// Hover 单元格 - rgba(0, 0, 0, 0.08)
|
|
311
|
+
cellHovered: {
|
|
312
|
+
backgroundColor: 'rgba(0, 0, 0, 0.08)',
|
|
313
|
+
},
|
|
314
|
+
// Column Stats 行(Sum/Mean)- 纯白色背景
|
|
315
|
+
columnStatsRow: {
|
|
316
|
+
backgroundColor: '#FFFFFF', // 纯白色
|
|
317
|
+
borderBottomColor: colors.colorTableBorder,
|
|
318
|
+
},
|
|
319
|
+
// Row Stats 列 - 白色背景
|
|
320
|
+
rowStatsCell: {
|
|
321
|
+
backgroundColor: colors.colorSurface1, // 白色
|
|
322
|
+
borderBottomColor: colors.colorTableBorder,
|
|
323
|
+
borderRightColor: colors.colorTableBorder,
|
|
324
|
+
},
|
|
325
|
+
cell: {
|
|
326
|
+
borderRightColor: colors.colorTableBorder,
|
|
327
|
+
},
|
|
328
|
+
rowLabel: {
|
|
329
|
+
color: colors.colorTextPrimary,
|
|
330
|
+
},
|
|
331
|
+
cellQuantity: {
|
|
332
|
+
color: colors.colorTextPrimary,
|
|
333
|
+
},
|
|
334
|
+
cellAmount: {
|
|
335
|
+
color: colors.colorTextSecondary,
|
|
336
|
+
},
|
|
337
|
+
statsLabel: {
|
|
338
|
+
color: columnStatsLabelStyle?.textColor || colors.colorTextBrand, // 棕色
|
|
339
|
+
fontSize: columnStatsLabelStyle?.fontSize || 14,
|
|
340
|
+
fontWeight: columnStatsLabelStyle?.fontWeight || '600',
|
|
341
|
+
},
|
|
342
|
+
paginationContainer: {
|
|
343
|
+
borderTopColor: colors.colorTableBorder,
|
|
344
|
+
backgroundColor: colors.colorSurface,
|
|
345
|
+
},
|
|
346
|
+
// Column Stats 分隔线
|
|
347
|
+
columnStatsDivider: {
|
|
348
|
+
borderTopWidth: 2,
|
|
349
|
+
borderTopColor: colors.colorTableBorder,
|
|
350
|
+
},
|
|
351
|
+
}), [colors, headerStyle, rowStatsHeaderStyle, columnStatsLabelStyle]);
|
|
117
352
|
|
|
118
353
|
// 构建矩阵数据
|
|
119
354
|
const matrix = useMemo(() => {
|
|
@@ -130,126 +365,158 @@ export function StatisticsTable({
|
|
|
130
365
|
return data;
|
|
131
366
|
}, [rows, columns, cells]);
|
|
132
367
|
|
|
133
|
-
//
|
|
368
|
+
// 计算行统计
|
|
134
369
|
const rowStats = useMemo(() => {
|
|
135
370
|
if (!showRowStats) return null;
|
|
136
371
|
return rows.map(row => {
|
|
137
372
|
const rowCells = columns
|
|
138
|
-
.filter(col => !col.isAction)
|
|
373
|
+
.filter(col => !col.isAction)
|
|
139
374
|
.map(col => matrix[row.key]?.[col.key] || { quantity: 0, amount: 0 });
|
|
140
375
|
const sumQuantity = rowCells.reduce((sum, cell) => sum + cell.quantity, 0);
|
|
141
376
|
const sumAmount = rowCells.reduce((sum, cell) => sum + cell.amount, 0);
|
|
142
377
|
const count = rowCells.length || 1;
|
|
143
|
-
|
|
144
|
-
const meanAmount = sumAmount / count;
|
|
145
|
-
return { sumQuantity, sumAmount, meanQuantity, meanAmount };
|
|
378
|
+
return { sumQuantity, sumAmount, meanQuantity: sumQuantity / count, meanAmount: sumAmount / count };
|
|
146
379
|
});
|
|
147
380
|
}, [rows, columns, matrix, showRowStats]);
|
|
148
381
|
|
|
149
|
-
//
|
|
382
|
+
// 计算列统计
|
|
150
383
|
const columnStats = useMemo(() => {
|
|
151
384
|
if (!showColumnStats) return null;
|
|
152
385
|
return columns
|
|
153
|
-
.filter(col => !col.isAction)
|
|
386
|
+
.filter(col => !col.isAction)
|
|
154
387
|
.map(col => {
|
|
155
388
|
const colCells = rows.map(row => matrix[row.key]?.[col.key] || { quantity: 0, amount: 0 });
|
|
156
389
|
const sumQuantity = colCells.reduce((sum, cell) => sum + cell.quantity, 0);
|
|
157
390
|
const sumAmount = colCells.reduce((sum, cell) => sum + cell.amount, 0);
|
|
158
391
|
const count = colCells.length || 1;
|
|
159
|
-
|
|
160
|
-
const meanAmount = sumAmount / count;
|
|
161
|
-
return { sumQuantity, sumAmount, meanQuantity, meanAmount };
|
|
392
|
+
return { sumQuantity, sumAmount, meanQuantity: sumQuantity / count, meanAmount: sumAmount / count };
|
|
162
393
|
});
|
|
163
394
|
}, [rows, columns, matrix, showColumnStats]);
|
|
164
395
|
|
|
165
|
-
// 获取列宽
|
|
166
396
|
const getColWidth = useCallback((column: StatisticsTableColumn) => {
|
|
167
|
-
if (getColumnWidth)
|
|
168
|
-
return getColumnWidth(column.key, column);
|
|
169
|
-
}
|
|
397
|
+
if (getColumnWidth) return getColumnWidth(column.key, column);
|
|
170
398
|
return column.width || 150;
|
|
171
399
|
}, [getColumnWidth]);
|
|
172
400
|
|
|
173
|
-
// 获取行高
|
|
174
401
|
const getRowHeightValue = useCallback((row: { key: string; label: string; data?: any; height?: number }) => {
|
|
175
402
|
if (row.height) return row.height;
|
|
176
|
-
if (getRowHeight)
|
|
177
|
-
return getRowHeight(row.key, row.data);
|
|
178
|
-
}
|
|
403
|
+
if (getRowHeight) return getRowHeight(row.key, row.data);
|
|
179
404
|
return rowHeight;
|
|
180
405
|
}, [rowHeight, getRowHeight]);
|
|
181
406
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
407
|
+
// 处理滚动
|
|
408
|
+
const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
184
409
|
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
410
|
+
setScrollState({
|
|
411
|
+
left: contentOffset.x,
|
|
412
|
+
top: contentOffset.y,
|
|
413
|
+
isAtStart: contentOffset.x <= 0,
|
|
414
|
+
isAtEnd: contentOffset.x + layoutMeasurement.width >= contentSize.width - 1,
|
|
415
|
+
isAtTop: contentOffset.y <= 0,
|
|
416
|
+
isAtBottom: contentOffset.y + layoutMeasurement.height >= contentSize.height - 1,
|
|
417
|
+
});
|
|
189
418
|
}, []);
|
|
190
419
|
|
|
191
|
-
//
|
|
192
|
-
const
|
|
193
|
-
const { contentOffset
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
setVerticalScrollState({ isAtStart, isAtEnd });
|
|
198
|
-
}, []);
|
|
420
|
+
// 同步 Row Stats 滚动
|
|
421
|
+
const handleBodyScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
422
|
+
const { contentOffset } = event.nativeEvent;
|
|
423
|
+
rowStatsScrollRef.current?.scrollTo({ y: contentOffset.y, animated: false });
|
|
424
|
+
handleScroll(event);
|
|
425
|
+
}, [handleScroll]);
|
|
199
426
|
|
|
200
|
-
//
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return 'right';
|
|
427
|
+
// 处理单元格 hover
|
|
428
|
+
const handleCellHover = useCallback((rowKey: string, columnKey: string, isHovered: boolean) => {
|
|
429
|
+
if (isHovered) {
|
|
430
|
+
setHoveredCell({ rowKey, columnKey });
|
|
431
|
+
} else {
|
|
432
|
+
setHoveredCell(null);
|
|
207
433
|
}
|
|
208
|
-
|
|
209
|
-
}, []);
|
|
434
|
+
onCellHover?.(rowKey, columnKey, isHovered);
|
|
435
|
+
}, [onCellHover]);
|
|
436
|
+
|
|
437
|
+
// 获取单元格 tooltip 内容
|
|
438
|
+
const getCellTooltipContent = useCallback((rowKey: string, columnKey: string, cell: StatisticsTableCell, column: StatisticsTableColumn): TooltipContent | null => {
|
|
439
|
+
if (renderCellTooltip) {
|
|
440
|
+
return renderCellTooltip(rowKey, columnKey, cell, column);
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
title: column.title,
|
|
444
|
+
items: [
|
|
445
|
+
{ label: 'Quantity', value: formatQuantity(cell.quantity) },
|
|
446
|
+
{ label: 'Amount', value: formatAmount(cell.amount) },
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
}, [renderCellTooltip, formatQuantity, formatAmount]);
|
|
450
|
+
|
|
451
|
+
// 获取行统计 tooltip
|
|
452
|
+
const getRowStatsTooltipContent = useCallback((rowKey: string, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }, storeCount: number): TooltipContent | null => {
|
|
453
|
+
if (renderRowStatsTooltip) {
|
|
454
|
+
return renderRowStatsTooltip(rowKey, stats, storeCount);
|
|
455
|
+
}
|
|
456
|
+
const row = rows.find(r => r.key === rowKey);
|
|
457
|
+
return {
|
|
458
|
+
title: row?.label || rowKey,
|
|
459
|
+
subtitle: `${storeCount} stores`,
|
|
460
|
+
items: [
|
|
461
|
+
{ label: 'Sum', value: formatAmount(stats.sumAmount) },
|
|
462
|
+
{ label: 'Mean', value: formatAmount(stats.meanAmount) },
|
|
463
|
+
],
|
|
464
|
+
};
|
|
465
|
+
}, [renderRowStatsTooltip, formatAmount, rows]);
|
|
466
|
+
|
|
467
|
+
// 获取列统计 tooltip
|
|
468
|
+
const getColumnStatsTooltipContent = useCallback((columnKey: string, column: StatisticsTableColumn, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }): TooltipContent | null => {
|
|
469
|
+
if (renderColumnStatsTooltip) {
|
|
470
|
+
return renderColumnStatsTooltip(columnKey, column, stats);
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
title: column.title,
|
|
474
|
+
items: [
|
|
475
|
+
{ label: 'Sum', value: formatAmount(stats.sumAmount) },
|
|
476
|
+
{ label: 'Mean', value: formatAmount(stats.meanAmount) },
|
|
477
|
+
],
|
|
478
|
+
};
|
|
479
|
+
}, [renderColumnStatsTooltip, formatAmount]);
|
|
210
480
|
|
|
211
481
|
// 渲染单元格内容
|
|
212
482
|
const renderCellContent = useCallback((column: StatisticsTableColumn, cell: any, rowData?: any) => {
|
|
213
|
-
|
|
214
|
-
if (column.render) {
|
|
215
|
-
return column.render(cell, rowData);
|
|
216
|
-
}
|
|
483
|
+
if (column.render) return column.render(cell, rowData);
|
|
217
484
|
|
|
218
|
-
// Action 列
|
|
219
485
|
if (column.isAction && rowActions) {
|
|
220
486
|
return (
|
|
221
487
|
<View style={styles.actionContainer}>
|
|
222
488
|
{rowActions.map((action, index) => (
|
|
223
489
|
<TouchableOpacity
|
|
224
490
|
key={index}
|
|
225
|
-
style={styles.actionButton}
|
|
491
|
+
style={[styles.actionButton, { backgroundColor: colors.colorTextAccent }]}
|
|
226
492
|
onPress={() => action.onPress(cell.rowKey, rowData)}
|
|
227
493
|
activeOpacity={0.7}
|
|
228
494
|
>
|
|
229
|
-
<Text style={styles.actionButtonText}>{action.label}</Text>
|
|
495
|
+
<Text style={[styles.actionButtonText, { color: colors.colorTextInvert }]}>{action.label}</Text>
|
|
230
496
|
</TouchableOpacity>
|
|
231
497
|
))}
|
|
232
498
|
</View>
|
|
233
499
|
);
|
|
234
500
|
}
|
|
235
501
|
|
|
236
|
-
// 默认渲染
|
|
237
|
-
const align = getCellAlignment(column);
|
|
238
502
|
return (
|
|
239
|
-
<View style={
|
|
240
|
-
<Text style={[styles.cellQuantity,
|
|
503
|
+
<View style={styles.cellContentContainer}>
|
|
504
|
+
<Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
|
|
241
505
|
{formatQuantity(cell.quantity)}
|
|
242
506
|
</Text>
|
|
243
|
-
<Text style={[styles.cellAmount,
|
|
507
|
+
<Text style={[styles.cellAmount, themedStyles.cellAmount]}>
|
|
244
508
|
{formatAmount(cell.amount)}
|
|
245
509
|
</Text>
|
|
246
510
|
</View>
|
|
247
511
|
);
|
|
248
|
-
}, [rowActions,
|
|
512
|
+
}, [rowActions, formatQuantity, formatAmount, colors, themedStyles]);
|
|
513
|
+
|
|
514
|
+
// 计算统计列宽度
|
|
515
|
+
const statsColumnsWidth = actualShowRowStats ? statsColumnWidth * 2 : 0;
|
|
249
516
|
|
|
250
517
|
if (loading) {
|
|
251
518
|
return (
|
|
252
|
-
<View style={[styles.container, style]}>
|
|
519
|
+
<View style={[styles.container, themedStyles.container, style]}>
|
|
253
520
|
<View style={styles.loadingRow}>
|
|
254
521
|
<Text>Loading...</Text>
|
|
255
522
|
</View>
|
|
@@ -259,298 +526,368 @@ export function StatisticsTable({
|
|
|
259
526
|
|
|
260
527
|
if (rows.length === 0 || columns.length === 0) {
|
|
261
528
|
return (
|
|
262
|
-
<View style={[styles.container, style]}>
|
|
529
|
+
<View style={[styles.container, themedStyles.container, style]}>
|
|
263
530
|
<View style={styles.emptyRow}>
|
|
264
|
-
<Text style={styles.emptyText}>{emptyText}</Text>
|
|
531
|
+
<Text style={[styles.emptyText, { color: colors.colorTextPlaceholder }]}>{emptyText}</Text>
|
|
265
532
|
</View>
|
|
266
533
|
</View>
|
|
267
534
|
);
|
|
268
535
|
}
|
|
269
536
|
|
|
270
|
-
// 虚拟滚动实现(简化版)
|
|
271
537
|
const visibleRows = enableVirtualization
|
|
272
538
|
? rows.slice(0, Math.ceil(maxHeight / virtualRowHeight))
|
|
273
539
|
: rows;
|
|
274
540
|
|
|
541
|
+
// Web 阴影样式
|
|
542
|
+
const webShadowStyle = Platform.OS === 'web' ? {
|
|
543
|
+
leftShadow: { boxShadow: '2px 0 6px 4px rgba(0, 0, 0, 0.08)' } as any,
|
|
544
|
+
rightShadow: { boxShadow: '-2px 0 6px 4px rgba(0, 0, 0, 0.08)' } as any,
|
|
545
|
+
topShadow: { boxShadow: '0 2px 6px 4px rgba(0, 0, 0, 0.08)' } as any,
|
|
546
|
+
bottomShadow: { boxShadow: '0 -2px 6px 4px rgba(0, 0, 0, 0.08)' } as any,
|
|
547
|
+
} : {};
|
|
548
|
+
|
|
549
|
+
// 隐藏滚动条的样式
|
|
550
|
+
const hideScrollbarStyle = Platform.OS === 'web' ? {
|
|
551
|
+
scrollbarWidth: 'none',
|
|
552
|
+
msOverflowStyle: 'none',
|
|
553
|
+
} as any : {};
|
|
554
|
+
|
|
555
|
+
const columnStatsHeight = actualShowColumnStats ? 120 : 0;
|
|
556
|
+
const storeCount = columns.filter(c => !c.isAction).length;
|
|
557
|
+
|
|
275
558
|
return (
|
|
276
|
-
<View style={[styles.container, style]}>
|
|
559
|
+
<View style={[styles.container, themedStyles.container, style]}>
|
|
277
560
|
{/* 左边阴影 */}
|
|
278
|
-
{enableScrollShadow && !
|
|
561
|
+
{enableScrollShadow && !scrollState.isAtStart && (
|
|
279
562
|
<View style={[
|
|
563
|
+
styles.shadowBar,
|
|
280
564
|
styles.shadowLeft,
|
|
281
|
-
|
|
282
|
-
width: scrollShadowSize,
|
|
283
|
-
backgroundColor: 'transparent',
|
|
284
|
-
shadowColor: scrollShadowColor,
|
|
285
|
-
}
|
|
565
|
+
Platform.OS === 'web' && webShadowStyle.leftShadow,
|
|
286
566
|
]} />
|
|
287
567
|
)}
|
|
288
568
|
|
|
289
569
|
{/* 右边阴影 */}
|
|
290
|
-
{enableScrollShadow && !
|
|
570
|
+
{enableScrollShadow && !scrollState.isAtEnd && (
|
|
291
571
|
<View style={[
|
|
572
|
+
styles.shadowBar,
|
|
292
573
|
styles.shadowRight,
|
|
293
|
-
{
|
|
294
|
-
|
|
295
|
-
backgroundColor: 'transparent',
|
|
296
|
-
shadowColor: scrollShadowColor,
|
|
297
|
-
}
|
|
574
|
+
{ right: actualShowRowStats ? statsColumnsWidth : 0 },
|
|
575
|
+
Platform.OS === 'web' && webShadowStyle.rightShadow,
|
|
298
576
|
]} />
|
|
299
577
|
)}
|
|
300
578
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
)}
|
|
320
|
-
|
|
321
|
-
{/* Table Header */}
|
|
322
|
-
<View style={styles.header}>
|
|
323
|
-
<View style={[styles.headerCell, { width: rowLabelWidth }]}>
|
|
324
|
-
<Text style={styles.headerText}></Text>
|
|
325
|
-
</View>
|
|
326
|
-
{columns.map((column) => {
|
|
327
|
-
const align = getCellAlignment(column, true);
|
|
328
|
-
const colWidth = getColWidth(column);
|
|
329
|
-
return (
|
|
330
|
-
<View
|
|
331
|
-
key={column.key}
|
|
332
|
-
style={[
|
|
333
|
-
styles.headerCell,
|
|
334
|
-
{
|
|
335
|
-
width: colWidth,
|
|
336
|
-
minWidth: column.minWidth,
|
|
337
|
-
maxWidth: column.maxWidth,
|
|
338
|
-
},
|
|
339
|
-
align === 'right' && styles.headerCellRight,
|
|
340
|
-
align === 'left' && styles.headerCellLeft,
|
|
341
|
-
]}
|
|
342
|
-
>
|
|
343
|
-
<Text style={[
|
|
344
|
-
styles.headerText,
|
|
345
|
-
align === 'right' && styles.headerTextRight,
|
|
346
|
-
align === 'left' && styles.headerTextLeft,
|
|
347
|
-
]}>
|
|
348
|
-
{column.title}
|
|
349
|
-
</Text>
|
|
350
|
-
</View>
|
|
351
|
-
);
|
|
352
|
-
})}
|
|
353
|
-
{showRowStats && (
|
|
354
|
-
<>
|
|
355
|
-
<View style={[styles.headerCell, { width: 120 }]}>
|
|
356
|
-
<Text style={styles.headerText}>Sum</Text>
|
|
357
|
-
</View>
|
|
358
|
-
<View style={[styles.headerCell, { width: 120 }]}>
|
|
359
|
-
<Text style={styles.headerText}>Mean</Text>
|
|
360
|
-
</View>
|
|
361
|
-
</>
|
|
362
|
-
)}
|
|
363
|
-
</View>
|
|
579
|
+
{/* 顶部阴影 */}
|
|
580
|
+
{enableScrollShadow && !scrollState.isAtTop && (
|
|
581
|
+
<View style={[
|
|
582
|
+
styles.shadowBarHorizontal,
|
|
583
|
+
styles.shadowTop,
|
|
584
|
+
Platform.OS === 'web' && webShadowStyle.topShadow,
|
|
585
|
+
]} />
|
|
586
|
+
)}
|
|
587
|
+
|
|
588
|
+
{/* 底部阴影 */}
|
|
589
|
+
{enableScrollShadow && !scrollState.isAtBottom && (
|
|
590
|
+
<View style={[
|
|
591
|
+
styles.shadowBarHorizontal,
|
|
592
|
+
styles.shadowBottom,
|
|
593
|
+
{ bottom: actualShowColumnStats ? columnStatsHeight : 0 },
|
|
594
|
+
Platform.OS === 'web' && webShadowStyle.bottomShadow,
|
|
595
|
+
]} />
|
|
596
|
+
)}
|
|
364
597
|
|
|
365
|
-
|
|
598
|
+
<View style={styles.tableContainer}>
|
|
599
|
+
{/* 主表格区域 */}
|
|
600
|
+
<View style={{ flex: 1 }}>
|
|
366
601
|
<ScrollView
|
|
367
|
-
ref={
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
onScroll={handleVerticalScroll}
|
|
602
|
+
ref={scrollViewRef}
|
|
603
|
+
horizontal
|
|
604
|
+
showsHorizontalScrollIndicator={showScrollIndicator}
|
|
605
|
+
onScroll={handleScroll}
|
|
372
606
|
scrollEventThrottle={16}
|
|
607
|
+
style={hideScrollbarStyle}
|
|
373
608
|
>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
</Text>
|
|
424
|
-
</View>
|
|
425
|
-
</View>
|
|
426
|
-
<View style={[styles.cell, styles.cellRight, { width: 120 }]}>
|
|
427
|
-
<View style={styles.cellContentContainer}>
|
|
428
|
-
<Text style={[styles.cellQuantity, styles.cellQuantityRight]}>
|
|
429
|
-
{formatQuantity(Math.round(rowStat.meanQuantity * 10) / 10)}
|
|
430
|
-
</Text>
|
|
431
|
-
<Text style={[styles.cellAmount, styles.cellAmountRight]}>
|
|
432
|
-
{formatAmount(rowStat.meanAmount)}
|
|
433
|
-
</Text>
|
|
434
|
-
</View>
|
|
609
|
+
<View>
|
|
610
|
+
{/* 表头 */}
|
|
611
|
+
<View style={[styles.header, themedStyles.header]}>
|
|
612
|
+
<View style={[styles.headerCell, themedStyles.headerCell, { width: rowLabelWidth }]}>
|
|
613
|
+
<Text style={[styles.headerText, themedStyles.headerText]}></Text>
|
|
614
|
+
</View>
|
|
615
|
+
{columns.map((column) => {
|
|
616
|
+
const colWidth = getColWidth(column);
|
|
617
|
+
return (
|
|
618
|
+
<View
|
|
619
|
+
key={column.key}
|
|
620
|
+
style={[styles.headerCell, themedStyles.headerCell, { width: colWidth }]}
|
|
621
|
+
>
|
|
622
|
+
<Text style={[styles.headerText, themedStyles.headerText]}>
|
|
623
|
+
{column.title}
|
|
624
|
+
</Text>
|
|
625
|
+
</View>
|
|
626
|
+
);
|
|
627
|
+
})}
|
|
628
|
+
</View>
|
|
629
|
+
|
|
630
|
+
{/* 表体 */}
|
|
631
|
+
<ScrollView
|
|
632
|
+
ref={bodyScrollViewRef}
|
|
633
|
+
style={{ maxHeight: maxHeight - columnStatsHeight }}
|
|
634
|
+
showsVerticalScrollIndicator={showScrollIndicator}
|
|
635
|
+
onScroll={handleBodyScroll}
|
|
636
|
+
scrollEventThrottle={16}
|
|
637
|
+
>
|
|
638
|
+
{visibleRows.map((row) => {
|
|
639
|
+
const currentRowHeight = getRowHeightValue(row);
|
|
640
|
+
const isClickable = !!onRowPress;
|
|
641
|
+
const isRowHovered = hoveredCell?.rowKey === row.key;
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<TouchableOpacity
|
|
645
|
+
key={row.key}
|
|
646
|
+
style={[
|
|
647
|
+
styles.row,
|
|
648
|
+
themedStyles.row,
|
|
649
|
+
{ minHeight: currentRowHeight },
|
|
650
|
+
isRowHovered && themedStyles.rowHovered,
|
|
651
|
+
]}
|
|
652
|
+
onPress={isClickable ? () => onRowPress(row.key, row.data) : undefined}
|
|
653
|
+
activeOpacity={isClickable ? 0.7 : 1}
|
|
654
|
+
disabled={!isClickable}
|
|
655
|
+
>
|
|
656
|
+
<View style={[styles.cell, themedStyles.cell, { width: rowLabelWidth }]}>
|
|
657
|
+
<Text style={[styles.rowLabel, themedStyles.rowLabel]}>{row.label}</Text>
|
|
435
658
|
</View>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
659
|
+
{columns.map((column) => {
|
|
660
|
+
const cell = matrix[row.key]?.[column.key] || { quantity: 0, amount: 0, rowKey: row.key, columnKey: column.key };
|
|
661
|
+
const colWidth = getColWidth(column);
|
|
662
|
+
const isHovered = hoveredCell?.rowKey === row.key && hoveredCell?.columnKey === column.key;
|
|
663
|
+
const tooltipContent = isHovered ? getCellTooltipContent(row.key, column.key, cell as StatisticsTableCell, column) : null;
|
|
664
|
+
|
|
665
|
+
return (
|
|
666
|
+
<Hoverable
|
|
667
|
+
key={column.key}
|
|
668
|
+
onHoverIn={() => handleCellHover(row.key, column.key, true)}
|
|
669
|
+
onHoverOut={() => handleCellHover(row.key, column.key, false)}
|
|
670
|
+
>
|
|
671
|
+
<View style={[
|
|
672
|
+
styles.cell,
|
|
673
|
+
themedStyles.cell,
|
|
674
|
+
{ width: colWidth },
|
|
675
|
+
isHovered && themedStyles.cellHovered,
|
|
676
|
+
]}>
|
|
677
|
+
{renderCellContent(column, cell, row.data)}
|
|
678
|
+
<CellTooltip
|
|
679
|
+
visible={isHovered}
|
|
680
|
+
content={tooltipContent}
|
|
681
|
+
style={{ bottom: '100%', left: 0, marginBottom: 8 }}
|
|
682
|
+
/>
|
|
683
|
+
</View>
|
|
684
|
+
</Hoverable>
|
|
685
|
+
);
|
|
686
|
+
})}
|
|
687
|
+
</TouchableOpacity>
|
|
688
|
+
);
|
|
689
|
+
})}
|
|
690
|
+
</ScrollView>
|
|
691
|
+
|
|
692
|
+
{/* Column Stats - 底部统计行 */}
|
|
693
|
+
{actualShowColumnStats && columnStats && (
|
|
694
|
+
<Animated.View style={[
|
|
695
|
+
styles.columnStatsContainer,
|
|
696
|
+
themedStyles.columnStatsDivider,
|
|
697
|
+
enableStatsAnimation && {
|
|
698
|
+
opacity: columnStatsAnimation,
|
|
699
|
+
transform: [{
|
|
700
|
+
translateY: columnStatsAnimation.interpolate({
|
|
701
|
+
inputRange: [0, 1],
|
|
702
|
+
outputRange: [60, 0],
|
|
703
|
+
}),
|
|
704
|
+
}],
|
|
705
|
+
},
|
|
706
|
+
]}>
|
|
707
|
+
{/* Sum 行 */}
|
|
708
|
+
<View style={[styles.row, themedStyles.columnStatsRow]}>
|
|
709
|
+
<View style={[styles.cell, themedStyles.cell, { width: rowLabelWidth }]}>
|
|
710
|
+
<Text style={[styles.statsLabel, themedStyles.statsLabel]}>{columnStatsLabels.sum}</Text>
|
|
711
|
+
</View>
|
|
712
|
+
{columns.map((column) => {
|
|
713
|
+
const colWidth = getColWidth(column);
|
|
714
|
+
if (column.isAction) {
|
|
715
|
+
return <View key={column.key} style={[styles.cell, { width: colWidth }]} />;
|
|
716
|
+
}
|
|
717
|
+
const stat = columnStats[columns.filter(c => !c.isAction).indexOf(column)];
|
|
718
|
+
const isHovered = hoveredCell?.rowKey === '__column_sum__' && hoveredCell?.columnKey === column.key;
|
|
719
|
+
const tooltipContent = isHovered && stat ? getColumnStatsTooltipContent(column.key, column, stat) : null;
|
|
720
|
+
|
|
721
|
+
return (
|
|
722
|
+
<Hoverable
|
|
723
|
+
key={column.key}
|
|
724
|
+
onHoverIn={() => handleCellHover('__column_sum__', column.key, true)}
|
|
725
|
+
onHoverOut={() => handleCellHover('__column_sum__', column.key, false)}
|
|
726
|
+
>
|
|
727
|
+
<View style={[
|
|
728
|
+
styles.cell,
|
|
729
|
+
themedStyles.cell,
|
|
730
|
+
{ width: colWidth },
|
|
731
|
+
isHovered && themedStyles.cellHovered,
|
|
732
|
+
]}>
|
|
733
|
+
<View style={styles.cellContentContainer}>
|
|
734
|
+
<Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
|
|
735
|
+
{formatQuantity(stat?.sumQuantity || 0)}
|
|
736
|
+
</Text>
|
|
737
|
+
<Text style={[styles.cellAmount, themedStyles.cellAmount]}>
|
|
738
|
+
{formatAmount(stat?.sumAmount || 0)}
|
|
739
|
+
</Text>
|
|
740
|
+
</View>
|
|
741
|
+
<CellTooltip
|
|
742
|
+
visible={isHovered}
|
|
743
|
+
content={tooltipContent}
|
|
744
|
+
style={{ bottom: '100%', left: 0, marginBottom: 8 }}
|
|
745
|
+
/>
|
|
746
|
+
</View>
|
|
747
|
+
</Hoverable>
|
|
748
|
+
);
|
|
749
|
+
})}
|
|
448
750
|
</View>
|
|
449
|
-
{
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
751
|
+
{/* Mean 行 */}
|
|
752
|
+
<View style={[styles.row, themedStyles.columnStatsRow]}>
|
|
753
|
+
<View style={[styles.cell, themedStyles.cell, { width: rowLabelWidth }]}>
|
|
754
|
+
<Text style={[styles.statsLabel, themedStyles.statsLabel]}>{columnStatsLabels.mean}</Text>
|
|
755
|
+
</View>
|
|
756
|
+
{columns.map((column) => {
|
|
757
|
+
const colWidth = getColWidth(column);
|
|
758
|
+
if (column.isAction) {
|
|
759
|
+
return <View key={column.key} style={[styles.cell, { width: colWidth }]} />;
|
|
760
|
+
}
|
|
761
|
+
const stat = columnStats[columns.filter(c => !c.isAction).indexOf(column)];
|
|
762
|
+
const isHovered = hoveredCell?.rowKey === '__column_mean__' && hoveredCell?.columnKey === column.key;
|
|
763
|
+
const tooltipContent = isHovered && stat ? getColumnStatsTooltipContent(column.key, column, stat) : null;
|
|
764
|
+
|
|
765
|
+
return (
|
|
766
|
+
<Hoverable
|
|
767
|
+
key={column.key}
|
|
768
|
+
onHoverIn={() => handleCellHover('__column_mean__', column.key, true)}
|
|
769
|
+
onHoverOut={() => handleCellHover('__column_mean__', column.key, false)}
|
|
770
|
+
>
|
|
771
|
+
<View style={[
|
|
772
|
+
styles.cell,
|
|
773
|
+
themedStyles.cell,
|
|
774
|
+
{ width: colWidth },
|
|
775
|
+
isHovered && themedStyles.cellHovered,
|
|
776
|
+
]}>
|
|
777
|
+
<View style={styles.cellContentContainer}>
|
|
778
|
+
<Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
|
|
779
|
+
{formatQuantity(Math.round((stat?.meanQuantity || 0) * 10) / 10)}
|
|
780
|
+
</Text>
|
|
781
|
+
<Text style={[styles.cellAmount, themedStyles.cellAmount]}>
|
|
782
|
+
{formatAmount(stat?.meanAmount || 0)}
|
|
783
|
+
</Text>
|
|
784
|
+
</View>
|
|
785
|
+
<CellTooltip
|
|
786
|
+
visible={isHovered}
|
|
787
|
+
content={tooltipContent}
|
|
788
|
+
style={{ bottom: '100%', left: 0, marginBottom: 8 }}
|
|
789
|
+
/>
|
|
790
|
+
</View>
|
|
791
|
+
</Hoverable>
|
|
792
|
+
);
|
|
793
|
+
})}
|
|
492
794
|
</View>
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
795
|
+
</Animated.View>
|
|
796
|
+
)}
|
|
797
|
+
</View>
|
|
798
|
+
</ScrollView>
|
|
799
|
+
</View>
|
|
800
|
+
|
|
801
|
+
{/* Row Stats - 右侧统计列 */}
|
|
802
|
+
{actualShowRowStats && (
|
|
803
|
+
<Animated.View style={[
|
|
804
|
+
styles.rowStatsContainer,
|
|
805
|
+
{ width: statsColumnsWidth },
|
|
806
|
+
Platform.OS === 'web' && { boxShadow: '-2px 0 6px rgba(0, 0, 0, 0.08)' } as any,
|
|
807
|
+
enableStatsAnimation && {
|
|
808
|
+
opacity: rowStatsAnimation,
|
|
809
|
+
transform: [{
|
|
810
|
+
translateX: rowStatsAnimation.interpolate({
|
|
811
|
+
inputRange: [0, 1],
|
|
812
|
+
outputRange: [statsColumnsWidth, 0],
|
|
813
|
+
}),
|
|
814
|
+
}],
|
|
815
|
+
},
|
|
816
|
+
]}>
|
|
817
|
+
{/* 统计列表头 */}
|
|
818
|
+
<View style={[styles.header, themedStyles.header]}>
|
|
819
|
+
<View style={[styles.headerCell, themedStyles.statsHeaderCell, { width: statsColumnWidth }]}>
|
|
820
|
+
<Text style={[styles.headerText, themedStyles.statsHeaderText]}>{rowStatsHeaders.sum}</Text>
|
|
821
|
+
</View>
|
|
822
|
+
<View style={[styles.headerCell, themedStyles.statsHeaderCell, { width: statsColumnWidth }]}>
|
|
823
|
+
<Text style={[styles.headerText, themedStyles.statsHeaderText]}>{rowStatsHeaders.mean}</Text>
|
|
824
|
+
</View>
|
|
825
|
+
</View>
|
|
826
|
+
|
|
827
|
+
{/* 统计列数据 */}
|
|
828
|
+
<ScrollView
|
|
829
|
+
ref={rowStatsScrollRef}
|
|
830
|
+
style={{ maxHeight: maxHeight - columnStatsHeight }}
|
|
831
|
+
showsVerticalScrollIndicator={false}
|
|
832
|
+
scrollEnabled={false}
|
|
833
|
+
>
|
|
834
|
+
{visibleRows.map((row, rowIndex) => {
|
|
835
|
+
const rowStat = rowStats?.[rowIndex];
|
|
836
|
+
const currentRowHeight = getRowHeightValue(row);
|
|
837
|
+
const isRowHovered = hoveredCell?.rowKey === row.key;
|
|
838
|
+
const isSumHovered = hoveredCell?.rowKey === row.key && hoveredCell?.columnKey === '__row_sum__';
|
|
839
|
+
const sumTooltipContent = isSumHovered && rowStat ? getRowStatsTooltipContent(row.key, rowStat, storeCount) : null;
|
|
840
|
+
|
|
841
|
+
return (
|
|
842
|
+
<View key={row.key} style={[
|
|
843
|
+
styles.row,
|
|
844
|
+
themedStyles.rowStatsCell,
|
|
845
|
+
{ minHeight: currentRowHeight },
|
|
846
|
+
isRowHovered && themedStyles.rowHovered,
|
|
847
|
+
]}>
|
|
848
|
+
<Hoverable
|
|
849
|
+
onHoverIn={() => handleCellHover(row.key, '__row_sum__', true)}
|
|
850
|
+
onHoverOut={() => handleCellHover(row.key, '__row_sum__', false)}
|
|
851
|
+
>
|
|
852
|
+
<View style={[styles.cell, themedStyles.cell, { width: statsColumnWidth }]}>
|
|
515
853
|
<View style={styles.cellContentContainer}>
|
|
516
|
-
<Text style={[styles.cellQuantity,
|
|
517
|
-
{formatQuantity(
|
|
854
|
+
<Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
|
|
855
|
+
{formatQuantity(rowStat?.sumQuantity || 0)}
|
|
518
856
|
</Text>
|
|
519
|
-
<Text style={[styles.cellAmount,
|
|
520
|
-
{formatAmount(
|
|
857
|
+
<Text style={[styles.cellAmount, themedStyles.cellAmount]}>
|
|
858
|
+
{formatAmount(rowStat?.sumAmount || 0)}
|
|
521
859
|
</Text>
|
|
522
860
|
</View>
|
|
861
|
+
<CellTooltip
|
|
862
|
+
visible={isSumHovered}
|
|
863
|
+
content={sumTooltipContent}
|
|
864
|
+
style={{ bottom: '100%', right: 0, marginBottom: 8 }}
|
|
865
|
+
/>
|
|
523
866
|
</View>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
867
|
+
</Hoverable>
|
|
868
|
+
<View style={[styles.cell, themedStyles.cell, { width: statsColumnWidth }]}>
|
|
869
|
+
<View style={styles.cellContentContainer}>
|
|
870
|
+
<Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
|
|
871
|
+
{formatQuantity(Math.round((rowStat?.meanQuantity || 0) * 10) / 10)}
|
|
872
|
+
</Text>
|
|
873
|
+
<Text style={[styles.cellAmount, themedStyles.cellAmount]}>
|
|
874
|
+
{formatAmount(rowStat?.meanAmount || 0)}
|
|
875
|
+
</Text>
|
|
876
|
+
</View>
|
|
877
|
+
</View>
|
|
878
|
+
</View>
|
|
879
|
+
);
|
|
880
|
+
})}
|
|
881
|
+
</ScrollView>
|
|
536
882
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
{
|
|
542
|
-
height: scrollShadowSize,
|
|
543
|
-
backgroundColor: 'transparent',
|
|
544
|
-
shadowColor: scrollShadowColor,
|
|
545
|
-
}
|
|
546
|
-
]} />
|
|
547
|
-
)}
|
|
548
|
-
</View>
|
|
549
|
-
</ScrollView>
|
|
883
|
+
{/* 由于 column stats 和 row stats 互斥,这里不需要底部占位 */}
|
|
884
|
+
</Animated.View>
|
|
885
|
+
)}
|
|
886
|
+
</View>
|
|
550
887
|
|
|
551
888
|
{/* Pagination */}
|
|
552
889
|
{pagination !== false && pagination && (
|
|
553
|
-
<View style={styles.paginationContainer}>
|
|
890
|
+
<View style={[styles.paginationContainer, themedStyles.paginationContainer]}>
|
|
554
891
|
<Pagination
|
|
555
892
|
totalCount={`${pagination.total} items`}
|
|
556
893
|
pageParams={{
|
|
@@ -571,93 +908,58 @@ export function StatisticsTable({
|
|
|
571
908
|
|
|
572
909
|
const styles = StyleSheet.create({
|
|
573
910
|
container: {
|
|
574
|
-
|
|
575
|
-
borderRadius: 8,
|
|
911
|
+
borderRadius: 12,
|
|
576
912
|
overflow: 'hidden',
|
|
577
913
|
},
|
|
914
|
+
tableContainer: {
|
|
915
|
+
flexDirection: 'row',
|
|
916
|
+
},
|
|
578
917
|
header: {
|
|
579
918
|
flexDirection: 'row',
|
|
580
|
-
|
|
581
|
-
borderBottomWidth: 2,
|
|
582
|
-
borderBottomColor: '#E0E0E0',
|
|
919
|
+
borderBottomWidth: 1,
|
|
583
920
|
},
|
|
584
921
|
headerCell: {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
borderRightColor: '#E0E0E0',
|
|
922
|
+
paddingVertical: 16,
|
|
923
|
+
paddingHorizontal: 12,
|
|
588
924
|
justifyContent: 'center',
|
|
589
925
|
alignItems: 'center',
|
|
590
926
|
},
|
|
591
|
-
headerCellLeft: {
|
|
592
|
-
alignItems: 'flex-start',
|
|
593
|
-
},
|
|
594
|
-
headerCellRight: {
|
|
595
|
-
alignItems: 'flex-end',
|
|
596
|
-
},
|
|
597
927
|
headerText: {
|
|
598
928
|
fontSize: 14,
|
|
599
|
-
fontWeight: '
|
|
600
|
-
color: '#2C3E50',
|
|
929
|
+
fontWeight: '500',
|
|
601
930
|
textAlign: 'center',
|
|
602
931
|
},
|
|
603
|
-
headerTextLeft: {
|
|
604
|
-
textAlign: 'left',
|
|
605
|
-
},
|
|
606
|
-
headerTextRight: {
|
|
607
|
-
textAlign: 'right',
|
|
608
|
-
},
|
|
609
932
|
row: {
|
|
610
933
|
flexDirection: 'row',
|
|
611
934
|
borderBottomWidth: 1,
|
|
612
|
-
borderBottomColor: '#F0F0F0',
|
|
613
|
-
backgroundColor: '#FFFFFF',
|
|
614
|
-
},
|
|
615
|
-
statsRow: {
|
|
616
|
-
backgroundColor: '#F9F9F9',
|
|
617
935
|
},
|
|
618
936
|
cell: {
|
|
619
|
-
|
|
937
|
+
paddingVertical: 16,
|
|
938
|
+
paddingHorizontal: 12,
|
|
620
939
|
justifyContent: 'center',
|
|
621
940
|
alignItems: 'center',
|
|
622
|
-
borderRightWidth: 1,
|
|
623
|
-
borderRightColor: '#F0F0F0',
|
|
624
|
-
},
|
|
625
|
-
cellLeft: {
|
|
626
|
-
alignItems: 'flex-start',
|
|
627
|
-
},
|
|
628
|
-
cellRight: {
|
|
629
|
-
alignItems: 'flex-end',
|
|
630
941
|
},
|
|
631
942
|
cellContentContainer: {
|
|
632
943
|
width: '100%',
|
|
944
|
+
alignItems: 'center',
|
|
633
945
|
},
|
|
634
946
|
rowLabel: {
|
|
635
947
|
fontSize: 14,
|
|
636
948
|
fontWeight: '500',
|
|
637
|
-
color: '#2C3E50',
|
|
638
949
|
},
|
|
639
950
|
cellQuantity: {
|
|
640
|
-
fontSize:
|
|
641
|
-
fontWeight: '
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
textAlign: 'left',
|
|
645
|
-
},
|
|
646
|
-
cellQuantityRight: {
|
|
647
|
-
textAlign: 'right',
|
|
951
|
+
fontSize: 16,
|
|
952
|
+
fontWeight: '600',
|
|
953
|
+
marginBottom: 2,
|
|
954
|
+
textAlign: 'center',
|
|
648
955
|
},
|
|
649
956
|
cellAmount: {
|
|
650
957
|
fontSize: 12,
|
|
651
|
-
|
|
652
|
-
textAlign: 'left',
|
|
653
|
-
},
|
|
654
|
-
cellAmountRight: {
|
|
655
|
-
textAlign: 'right',
|
|
958
|
+
textAlign: 'center',
|
|
656
959
|
},
|
|
657
960
|
statsLabel: {
|
|
658
961
|
fontSize: 14,
|
|
659
962
|
fontWeight: '600',
|
|
660
|
-
color: '#2C3E50',
|
|
661
963
|
},
|
|
662
964
|
actionContainer: {
|
|
663
965
|
flexDirection: 'row',
|
|
@@ -667,13 +969,11 @@ const styles = StyleSheet.create({
|
|
|
667
969
|
actionButton: {
|
|
668
970
|
paddingHorizontal: 12,
|
|
669
971
|
paddingVertical: 6,
|
|
670
|
-
backgroundColor: '#FF6B00',
|
|
671
972
|
borderRadius: 4,
|
|
672
973
|
},
|
|
673
974
|
actionButtonText: {
|
|
674
975
|
fontSize: 12,
|
|
675
976
|
fontWeight: '600',
|
|
676
|
-
color: '#FFFFFF',
|
|
677
977
|
},
|
|
678
978
|
loadingRow: {
|
|
679
979
|
padding: 40,
|
|
@@ -687,57 +987,48 @@ const styles = StyleSheet.create({
|
|
|
687
987
|
},
|
|
688
988
|
emptyText: {
|
|
689
989
|
fontSize: 14,
|
|
690
|
-
color: '#999',
|
|
691
990
|
},
|
|
692
991
|
paginationContainer: {
|
|
693
992
|
padding: 16,
|
|
694
993
|
borderTopWidth: 1,
|
|
695
|
-
borderTopColor: '#E0E0E0',
|
|
696
|
-
backgroundColor: '#FAFAFA',
|
|
697
994
|
},
|
|
698
|
-
//
|
|
699
|
-
|
|
995
|
+
// Row Stats 容器
|
|
996
|
+
rowStatsContainer: {
|
|
997
|
+
position: 'relative',
|
|
998
|
+
},
|
|
999
|
+
// Column Stats 容器
|
|
1000
|
+
columnStatsContainer: {
|
|
1001
|
+
// 顶部边框由 themedStyles.columnStatsDivider 提供
|
|
1002
|
+
},
|
|
1003
|
+
// 垂直阴影条
|
|
1004
|
+
shadowBar: {
|
|
700
1005
|
position: 'absolute',
|
|
1006
|
+
zIndex: 10,
|
|
1007
|
+
width: 1,
|
|
1008
|
+
backgroundColor: 'transparent',
|
|
1009
|
+
},
|
|
1010
|
+
shadowLeft: {
|
|
701
1011
|
left: 0,
|
|
702
1012
|
top: 0,
|
|
703
1013
|
bottom: 0,
|
|
704
|
-
zIndex: 10,
|
|
705
|
-
shadowOffset: { width: 2, height: 0 },
|
|
706
|
-
shadowOpacity: 0.15,
|
|
707
|
-
shadowRadius: 4,
|
|
708
|
-
elevation: 4,
|
|
709
1014
|
},
|
|
710
1015
|
shadowRight: {
|
|
711
|
-
position: 'absolute',
|
|
712
|
-
right: 0,
|
|
713
1016
|
top: 0,
|
|
714
1017
|
bottom: 0,
|
|
715
|
-
zIndex: 10,
|
|
716
|
-
shadowOffset: { width: -2, height: 0 },
|
|
717
|
-
shadowOpacity: 0.15,
|
|
718
|
-
shadowRadius: 4,
|
|
719
|
-
elevation: 4,
|
|
720
1018
|
},
|
|
721
|
-
|
|
1019
|
+
// 水平阴影条
|
|
1020
|
+
shadowBarHorizontal: {
|
|
722
1021
|
position: 'absolute',
|
|
1022
|
+
zIndex: 10,
|
|
1023
|
+
height: 1,
|
|
723
1024
|
left: 0,
|
|
724
1025
|
right: 0,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
shadowRadius: 4,
|
|
730
|
-
elevation: 4,
|
|
1026
|
+
backgroundColor: 'transparent',
|
|
1027
|
+
},
|
|
1028
|
+
shadowTop: {
|
|
1029
|
+
top: 56, // header height
|
|
731
1030
|
},
|
|
732
1031
|
shadowBottom: {
|
|
733
|
-
|
|
734
|
-
left: 0,
|
|
735
|
-
right: 0,
|
|
736
|
-
bottom: 0,
|
|
737
|
-
zIndex: 10,
|
|
738
|
-
shadowOffset: { width: 0, height: -2 },
|
|
739
|
-
shadowOpacity: 0.15,
|
|
740
|
-
shadowRadius: 4,
|
|
741
|
-
elevation: 4,
|
|
1032
|
+
// bottom 由动态样式设置
|
|
742
1033
|
},
|
|
743
1034
|
});
|